diff --git a/.gitattributes b/.gitattributes index 094765b3..4c251bbb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,20 +1,11 @@ # Exclude files from deployment: -## git files .gitattributes export-ignore .gitignore export-ignore -## duplicate documentation -PAGE.md export-ignore -README.md export-ignore -## build script -build_zip.sh export-ignore -## tools -tools export-ignore -## screenshots -/screenshots export-ignore -# Adjust GitHub linguist settings: -image_occlusion_enhanced/svg-edit/* linguist-vendored -image_occlusion_enhanced/Imaging/* linguist-vendored -image_occlusion_enhanced/uuid/* linguist-vendored -image_occlusion_enhanced/imagesize/* linguist-vendored -tools/fixiocards/* linguist-vendored -PAGE.md linguist-documentation \ No newline at end of file +.github export-ignore +docs export-ignore +screenshots export-ignore +addon.json export-ignore +# Adjust GitHub linguist settings +## Vendored +src/*/libaddon linguist-vendored +src/*/_vendor linguist-vendored diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..6deffc7d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ + +patreon: glutanimate +ko_fi: glutanimate diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..c3ad09f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +#### Problem description + +*Please describe the issue concisely in here. In case of an error: Walk us through the steps you took to get there. What happened? What did you expect to happen?* + + +#### Checklist + +*Please replace the space inside the brackets with an **x** if the following items apply:* + + - [ ] I've restarted Anki to see if it helps + - [ ] I've verified that I use the latest version of the add-on by redownloading it from AnkiWeb + - [ ] I've verified that I use the latest version of Anki by checking at https://apps.ankiweb.net#download + - [ ] I've tried to disable other add-ons to see if there are any interactions present + - [ ] My issue disappears when I hold shift while starting Anki. + - [ ] I've checked if anyone else reported this problem before by looking through the issue reports. I also checked to see if there is a section about known issues in the add-on description, documentation, or README. + + +#### Information about your Anki set-up + +*Please open Anki, go to Help → About, click on "Copy Debug Info", and paste the result between the backticks below (if the button does not appear you are using an older version of Anki 2.1 and will need to update first):* + +``` + +``` + +#### Error message (if any) + +*If you've received an error message, please copy and paste it between the backticks below:* + + +```python + +``` \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..612d776d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,35 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'enhancement' +assignees: '' + +--- + +#### Checklist + +*Please replace the space inside the brackets with an **x** if the following items apply:* + + - [ ] I've verified that I use the latest version of the add-on by redownloading it from AnkiWeb + - [ ] I've verified that I use the latest version of Anki by checking at https://apps.ankiweb.net#download + - [ ] I've checked if anyone else suggested this feature before by looking through the issue reports. + +#### Problem case + +*Is your feature request related to a problem? If so, please describe it here. E.g.: "My workflow is such and such, and this and that would help."* + + + +#### Solution + +*Concisely describe the solution you would like* + + +*Concisely describe alternatives you've considered (if any)* + + + +#### More information + +*Additional context: Add any other context or screenshots about the feature request here.* diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..cc64cd02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,21 @@ +--- +name: Question +about: Ask a question about this project +title: '' +labels: 'question' +assignees: '' + +--- + +#### Checklist + +*Please replace the space inside the brackets with an **x** if the following items apply:* + + - [ ] I've verified that I use the latest version of the add-on by redownloading it from AnkiWeb + - [ ] I've verified that I use the latest version of Anki by checking at https://apps.ankiweb.net#download + - [ ] I've checked if anyone else asked this question before by looking through the issue reports. + + +#### Your question + +*A clear and concise question about the add-on.* diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..14a59511 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +#### Description + +*Concisely describe what the pull request is trying to achieve. If pertinent, link to an existing issue report, or briefly explain the problem the PR is meant to solve. Feel free to attach screenshots or other media for UI-related changes.* + + +#### Checklist: + +*Please replace the space inside the brackets with an **x** and fill out the ellipses if the following items apply:* + +- [ ] I've read and understood the [contribution guidelines](./CONTRIBUTING.md) +- [ ] I've tested my changes against at least one of the following [Anki builds](https://apps.ankiweb.net/#download): + - [ ] Latest standard Anki 2.1 binary build + - [ ] Latest alternative Anki 2.1 binary build +- [ ] I've tested my changes on at least one of the following platforms: + - [ ] Linux, version: + - [ ] Windows, version: + - [ ] macOS, version: +- [ ] My changes potentially affect non-desktop platforms, of which I've tested: + - [ ] AnkiMobile, version: + - [ ] AnkiDroid, version: + - [ ] AnkiWeb diff --git a/.gitignore b/.gitignore index af35896d..bff9a7b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,10 @@ +# Linux FM +.hidden +.directory # Byte-compiled / optimized / DLL files *.pyo *.pyc __pycache__/ -# Anki -src/*/meta.json -# Build files -build -src/*/forms* -*-anki2*.zip # Python .python-version .mypy_cache/ @@ -19,29 +16,35 @@ venv/ ENV/ env.bak/ venv.bak/ +# Javascript +node_modules # IDEs *.sublime-project *.sublime-workspace .sublime-backup/ .idea/ .vscode/ +# Build files +build +src/*/gui/forms/anki* +src/*/gui/forms/LICENSE* +src/*/gui/resources/anki* +src/*/gui/resources/LICENSE* +*-anki2*.zip +# Dev tools +.bumpversion.cfg +labels.toml # Temp .gitold obsolete research -# Linux FM -.hidden -.directory -# Docs and tools -.github -docs/specs.md -docs/todo.md -docs/*.todo -docs/description.html -tools/test.py -# Add-on specific -docs/contribs.ui -image_occlusion_enhanced/svg-edit/build -image_occlusion_enhanced/svg-edit/Makefile -designer/ -image-occlusion-enhanced-*.zip +# Docs +todo +# Assets +resources/icons/optional +# Anki +src/*/meta.json +src/*/manifest.json +# Repo +src/image_occlusion_enhanced/svg-edit/svg-edit-master/ +.pytest_cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..87949982 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,491 @@ +# Changelog + +All notable changes to Image Occlusion Enhanced will be documented here. You can click on each release number to be directed to a detailed log of all code commits for that particular release. The download links will direct you to the GitHub release page, allowing you to manually install a release if you want. + +If you enjoy Image Occlusion Enhanced, please consider supporting my work on Patreon, or by buying me a cup of coffee :coffee:: + +

+      +

+ +:heart: My heartfelt thanks goes out to everyone who has supported this add-on through their tips, contributions, or any other means (you know who you are!). All of this would not have been possible without you. Thank you for being awesome! + +## [Unreleased] + +## [1.3.0-alpha6] - 2020-04-28 + +### [Download](https://github.com/glutanimate/image-occlusion-enhanced/releases/tag/v1.3.0-alpha6) + +### Fixed + +- Fixed note editing support on Anki 2.1.24+ (thanks to @zjosua for the fix!) +- Fixed a rare error that would occur when the add-on would update its template (thanks to Emma for the report!) + +## [1.3.0-alpha5] - 2019-09-11 + +### [Download](https://github.com/glutanimate/image-occlusion-enhanced/releases/tag/v1.3.0-alpha5) + +### Fixed + +- Handle malformatted and unrecognized image formats more graciously (thanks to Audrey for the report) + +## [1.3.0-alpha4] - 2019-04-14 + +### Added + +- Introduces .ankiaddon packaging format for GitHub builds + +### Fixed + +- Fixes error message when deleting current note in editing session (#87, thanks to @zjosua) +- Fixes rare error message when closing card browser + +## [1.3.0-alpha3] - 2018-12-24 + +### Fixed + +- Another uuid-related fix + +## [1.3.0-alpha2] - 2018-12-11 + +### Fixed + +- Quick hotfix for Anki 2.1.6 compatibility (now packages the UUID module which no longer seems to ship with Anki 2.1.6's Python distribution) + +## [1.3.0-alpha2] - 2018-12-11 + +### Fixed + +- Quick hotfix for Anki 2.1.6 compatibility (now packages the UUID module which no longer seems to ship with Anki 2.1.6's Python distribution) + +## [1.3.0-alpha1] - 2018-06-29 + +### Notes + +This marks the first official release of Image Occlusion Enhanced for Anki 2.1! Porting the add-on to Anki 2.1 has been [an arduous journey](https://github.com/glutanimate/image-occlusion-enhanced/projects/1), but I'm happy we're finally here. + +Before we dive into the actual changelog I would just like to thank each and everyone of you who has helped in this effort, either by [contributing to the codebase](https://github.com/glutanimate/image-occlusion-enhanced/graphs/contributors), filing [bug reports](https://github.com/glutanimate/image-occlusion-enhanced/issues), or by supporting my work directly through [tips](https://glutanimate.com/tip-jar/), [Patreon](https://www.patreon.com/glutanimate), or add-on commissions! + +In particular I would like to thank the following awesome people who support me on Patreon / have supported me at some point during the development of Image Occlusion Enhanced for Anki 2.1: + +- Blacky 372 +- Sebastián Ortega +- Edan Maor +- Peter Benisch + +**Important**: This is an Anki 2.1-only release (for now) + +### Added + +The primary focus in this release was Anki 2.1 compatibility, but v1.3.0 also comes with a number of nifty **new features** which I would like to highlight first: + +- You can now **occlude images in any note type you want**, either by **right clicking** on them and selecting the respective option, or by using the Image Occlusion button! +- As an added bonus: The new context menu introduced by the add-on will also allow you to **open any image with your default system viewer** – a great way to perform quick image editing tasks when needed. +- The masks editor now allows you to **add hints to your occlusion shapes**. In order to do so, simply create a text element on top of a shape and group it with the shape. +- You can now set a **custom hotkey** for invoking I/O. Gone are the days of conflicts with different keyboard locales! +- In-app **help screens** now guide users through the basic use of the add-on (including how to add cards, edit them, group masks, label items, etc.) + +(Some of the changes above will likely also be part of a future release of v1.3.0 for Anki 2.0.) + +### Fixed + +v1.3.0 for Anki 2.1 also comes with a plethora of **bug fixes** (some of the bugs fixed in this update have plagued I/O ever since its original release!): + +- Fix: Automatically remove accidentally drawn shapes. This addresses instances where users would end up with more cards than they should have because of invisible shapes drawn by the oversensitive editor component (especially with touch interfaces) +- Fix: Resolve issues with unicode characters in Anki path and/or image path. This should fix most of the problems users were experiencing with non-latin locales (e.g. the I/O editor screen remaining blank because SVG-Edit did not load, or various UnicodeError messages) +- Fix: More robust I/O editor instantiation. Should help address some of the stability issues users experienced over longer card creation sessions (e.g. needing to restart Anki to get I/O working again). +- and a large number of other smaller bug fixes and improvements + +### Changes + +There also some **changes to the workflow** in I/O v1.3.0 that you need to be aware of: + +- The default hotkey for invoking I/O is now Ctrl+Shift+O (customizable through the new settings entry) +- The card generation options have been renamed and simplified: You can now choose between "Hide All, Guess One" (used to be "Hide All, Reveal One") and "Hide One, Guess One" (used to be "Hide All, Reveal All"). My hope with these new names is that they will be more intuitive for new users. (thanks a lot to Tiago Barroso for the suggestion!) +- "Hide All, Reveal All" is no longer available as a mask generation option. With the mask reveal button introduced in recent I/O releases it no longer served much of a purpose and was mostly confusing new users as they expected it to work like a grouped occlusion of all shapes. + + Just in case you were using this option and are now wondering how to cover the same use cases: + + + In case you were using "Hide All, Reveal All" to uncover all labels on the back: Try to switch to using "Hide One, Guess One" coupled with the mask reveal button on the backside (hotkey: `G`) + + In case you were using "Hide All, Reveal All" to 'group' your shapes: Use the [actual grouping feature](https://github.com/glutanimate/image-occlusion-enhanced/wiki/Advanced-Use#grouping-items) instead + +### Limitations + +There are a number of known limitations to this alpha release that you need to be aware of: + +- Due to compatibility issues between SVG-Edit and the new Chromium renderer in Anki 2.1 some of the features in the masks editor no longer work correctly. My hope is to address these in the following beta release: + + [Pointer not changing to selection mode when clicking on shape](https://github.com/glutanimate/image-occlusion-enhanced/issues/54) + + [Path tool no longer working](https://github.com/glutanimate/image-occlusion-enhanced/issues/56) + + [https://github.com/glutanimate/image-occlusion-enhanced/issues/57](https://github.com/glutanimate/image-occlusion-enhanced/issues/57) +- Please do not invoke the add-on's settings menu while the I/O Editor is running. There is currently no support for updating I/O editing sessions at runtime, and while most settings will simply only not be applied, others might cause the add-on to stop working correctly until the editor session is restarted. The same applies to modifications to the add-on's note type via Anki's built-in note type manager. + +Of course there also bound to be some unforeseen bugs and regressions in the alpha. If you experience any of these please make sure to either report them on the add-on's [bug tracker](https://github.com/glutanimate/image-occlusion-enhanced/issues) or in the [official support thread](https://anki.tenderapp.com/discussions/add-ons/8295-image-occlusion-enhanced-official-thread). + +## [1.2.2] - 2017-04-04 + +### Fixed + +- Fixed: rare AttributeError when changing the image +- Fixed: GIFs should be supported on Windows and macOS now +- Fixed: incompatibility with upcoming release of "Quick not and deck buttons" add-on + +## [1.2.1] - 2017-02-14 + +### Fixed + +- Fixed: Unicode TypeError on Windows + + +## [1.2.0] - 2017-02-14 + +### Added + +- **New**: Hotkey for toggling masks on the answer side (`G`) +- **New**: Preserve scrollbar position when revealing the answer +- **New**: Reuse existing images when creating multiple occlusion sets from the same base image +- **New**: Limit image display height in editor fields in order to improve navigating notes in the card browser + +### Changed + +- Changed: Updated default field order to move the question mask below the original image. Should make it easier to identify each respective card in the browser. + +### Fixed + +- Fixed: Remove card margins on mobile devices +- Fixed: Increased size of mask toggle button on mobile devices + +### Notes + +Please note that the changes to the field order and card templates only apply to new installations of the add-on. I've decided against enforcing these changes on existing installations as that would undo any customizations you might have applied to the note type. If you'd like to update your cards with these changes, please follow [the instructions in the Wiki](https://github.com/Glutanimate/image-occlusion-enhanced/wiki/Troubleshooting#resetting-note-type-and-template-to-the-defaults) to reset your field order and card template to the (new) defaults. + + +## [1.1.1] - 2017-01-20 + +### Fixed + +- Clicking on a context menu entry would launch the web browser under some circumstances (thanks to PolymorphicVTach for the report) + + +## [1.1.0] - 2017-01-14 + +### Added + +- **New**: Grid-snapping for shapes can now be toggled via a button in the upper panel (hotkey: _Shift+S_) +- **New**: Panning tool (hotkey: _Q_) +- **New**: Control zoom levels with _+/-_ +- **New**: Reset zoom with _0_ + +### Fixed + +- Fixed: Error when marking the Image field as sticky (thanks to vidale3 for the report) +- Fixed: _Ctrl+Shift+T_ now focuses the tag field again + +### Changed + +- Improved: Images now use the available canvas space slightly more efficiently +- plus a number of smaller improvements and bug fixes + + +## [1.0.4] - 2016-12-14 + +### Added + +- add hotkeys for switching between layers (Ctrl+Shift+L and Ctrl+Shift+M) + +### Fixed + +- fix an encoding issue when editing labels + +## [1.0.3] - 2016-12-01 + +### Fixed + +- fix a runtime error that was occuring for some Windows users + + +## [1.0.2] - 2016-11-24 + +### Fixed + +- Fix unicode support in labels + + +## [1.0.1] - 2016-11-16 + +### Fixed + +- Restore proper window controls on Windows + + +## [1.0.0] - 2016-11-09 + +The most comprehensive update to _Image Occlusion Enhanced_ since its inception: + +### Added + +- **Modify Existing Notes** + - Need to remove or add a shape, update a field, or resize all masks? Now you can! +- **Change Images on the Fly** + - Simply switch to a different image to occlude right from the IO Editor +- **New Occlusion Mode** + - Hides all labels on the question side, and reveals all of them on the back + - The different occlusion modes now also follow a new naming scheme. It should be self-explanatory what each of them does, but you can hover over the respective button to get a more detailed description +- **Completely Reworked Note Type** + - 4 additional fields to give your notes enough space for all the extra information they might need + - New intuitive field order, with the Header and Image right on top. No more issues identifying your notes in the Browser! +- **Full Customization** + - You can now add as many fields to the note type as you like + - New Options entry for renaming default fields + +### Changed + +- **Updated Options Interface** + - More options, fewer bugs +- **Fully Rewritten Note Generator** + - Faster, more extensible, and less bug-prone +- **Performance Improvements** + - Smaller memory footprint in general use (by about 30MB) + +### Fixed + +- **Stability Improvements** + - Bug fixes everywhere + + +## [1.0.0-beta6] - 2016-11-07 + +### Added + +- **New**: Use a button instead of clicking the image to reveal all masks. The old method interfered with gesture support on mobile clients. +- **New**: Default action hotkey (Ctrl+Return) + +### Changed + +- **Other**: Set Extra fields to be note-specific by default + +## [1.0.0-beta5] - 2016-11-05 + +### Added + +- **New**: Reveal all occluded areas when clicking image on answer side – This is a somewhat of an experimental change as there's no official support for JavaScript in Anki. Note: Normally this would require an update to the IO note type, but given that this is an experimental change I decided not to force the update. Instead, feel free to test this new feature on an empty Anki profile. + +### Fixed + +- **Fix**: Force media sync when updating mask files +- **Fix**: Handle deleted IO note type more graciously + +### Changed + +- **Other**: Updated tooltips for occlusion types (@dgbeecher) + + +## [1.0.0-beta4] - 2016-10-18 + +### Changed + +- Updated the default card template + - The new template includes the two new extra fields and uses a more elaborate layout for all sections below the image. It also provides much needed adjustments of the styling for AnkiMobile and AnkiDroid + - **Important**: This will overwrite any previous changes to the card templates you might have performed. If you've used a previous Beta and customized your template or styles please make sure to back them up before installing this version. + - FWIW, drastic changes like this will only happen with Beta releases. If I ever see the need to change the template for a stable update I will implement it in a way that asks you for confirmation first. + +### Fixed + +- Fixed a number of smaller issues (thanks to @dgbeecher for reporting these!) + + +## [1.0.0-beta3] - 2016-10-15 + +### Added + +- New options for labels and lines +- New option for ignoring fields when editing +- New error dialog that provides a help button + +### Changed + +- More verbose tooltips when generating notes + +### Fixed + +- Line color and width should now be preserved when switching to a different tool +- Lots of smaller bug fixes + + +## [1.0.0-beta2] - 2016-10-13 + +### Fixed + +- Possible fix for a module import error on macOS + +## [1.0.0-beta1] - 2016-10-12 + +A tremendous version jump, I know, but this is the most comprehensive update to Image Occlusion since the release of _Image Occlusion Enhanced_ + +### Added + +- **Modify Existing Notes** + - Need to remove or add a shape, update a field, or resize all masks? Now you can! +- **Change Images on the Fly** + - Simply switch to a different image to occlude right from the IO Editor +- **New Occlusion Mode** + - Hides all labels on the question side, and reveals all of them on the back + - The different occlusion modes now also follow a new naming scheme. It should be self-explanatory what each of them does, but you can hover over the respective button to get a more detailed description +- **Completely Reworked Note Type** + - 4 additional fields to give your notes enough space for all the extra information they might need + - New intuitive field order, with the Header and Image right on top. No more issues identifying your notes in the Browser! +- **Full Customization** + - You can now add as many fields to the note type as you like + - New Options entry for renaming default fields + +### Changed + +- **Updated Options Interface** + - More options, fewer bugs +- **Fully Rewritten Note Generator** + - Faster, more extensible, and less bug-prone +- **Performance Improvements** + - Smaller memory footprint in general use (by about 30MB) + +### Fixed + +- **Stability Improvements** + - Bug fixes everywhere + +## [0.3.0] - 2016-09-28 + +### Changed + +- SVGEdit: fixed most of the random opacity changes +- SVGEdit: fixed some issues with the stroke and fill attributes +- SVGEdit: changed default text font +- SVGEdit: added initial fill color to the color palette +- SVGEdit: updated hotkey assignments to improve usability +- SVGEdit: added "Esc" hotkey to deselect current selection + + +## [0.2.6] - 2016-09-22 + +### Changed + +- improved IO note type sanity checks + +## [0.2.5] - 2016-09-12 + +### Changed + +- New add-on name +- Added link to new Wiki + +## [0.2.4] - 2016-08-25 + +### Fixed + +- Several bug fixes and improvements + +## [0.2.3] - 2016-05-19 + +### Changed + +- Update mask fill colour when upgrading from Image Occlusion 2.0 + + +## [0.2.2] - 2016-04-16 + +### Added + +- added support for preserving occlusions and labels when creating new notes based on old one + + +## [0.2.1] - 2016-04-04 + +### Fixed + +- fixed an encoding error on Windows + + +## [0.2.0] - 2016-04-03 + +### Changed + +- first release on AnkiWeb +- fixes a number of issues with SVG-Edit + + +## [0.1.4] - 2016-04-01 + +### Changed + +- improvements to the Masks editor +- bug fix: support for special characters in file names on Windows + + +## [0.1.3] - 2016-04-01 + +### Changed + +- update the Options window and Help link +- remember window geometry across sessions +- allow upgrading directly from I/O 2.0 +- a few miscellaneous fixes + + +## [0.1.2] - 2016-03-29 + +### Fixed + +- More fixes. + + +## [0.1.1] - 2016-03-28 + +### Fixed + +- A few smaller fixes. + + +## v0.1.0 - 2016-03-28 + +### Added + +- First release of Image Occlusion 2.0 Enhanced. +- Still needs a lot of testing! + + +[Unreleased]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.3.0-alpha5...HEAD +[1.3.0-alpha5]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.3.0-alpha4...v1.3.0-alpha5 +[1.3.0-alpha4]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.3.0-alpha3...v1.3.0-alpha4 +[1.3.0-alpha3]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.3.0-alpha2...v1.3.0-alpha3 +[1.3.0-alpha2]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.3.0-alpha1...v1.3.0-alpha2 +[1.3.0-alpha1]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.2.2...v1.3.0-alpha1 +[1.2.2]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.2.1...v1.2.2 +[1.2.1]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.2.0...v1.2.1 +[1.2.0]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.1.1...v1.2.0 +[1.1.1]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.1.0...v1.1.1 +[1.1.0]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.4...v1.1.0 +[1.0.4]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.3...v1.0.4 +[1.0.3]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.2...v1.0.3 +[1.0.2]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.1...v1.0.2 +[1.0.1]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.0-beta6...v1.0.0 +[1.0.0-beta6]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.0-beta5...v1.0.0-beta6 +[1.0.0-beta5]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.0-beta4...v1.0.0-beta5 +[1.0.0-beta4]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.0-beta3...v1.0.0-beta4 +[1.0.0-beta3]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.0-beta2...v1.0.0-beta3 +[1.0.0-beta2]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v1.0.0-beta1...v1.0.0-beta2 +[1.0.0-beta1]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.3.0...v1.0.0-beta1 +[0.3.0]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.2.6...v0.3.0 +[0.2.6]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.2.5...v0.2.6 +[0.2.5]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.2.4...v0.2.5 +[0.2.4]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.2.3...v0.2.4 +[0.2.3]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.2.2...v0.2.3 +[0.2.2]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.2.1...v0.2.2 +[0.2.1]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.1.4...v0.2.0 +[0.1.4]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.1.3...v0.1.4 +[0.1.3]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.1.2...v0.1.3 +[0.1.2]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/Glutanimate/image-occlusion-enhanced/compare/v0.1.0...v0.1.1 + +----- + +The format of this file is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cac2bd92 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +## How to Contribute to this Project + +Please see the [common contribution guidelines](https://github.com/glutanimate/docs/blob/master/anki/add-ons/CONTRIBUTING.md#how-to-contribute-to-my-anki-add-ons) for my Anki add-ons. + +Thanks! \ No newline at end of file diff --git a/Image Occlusion Enhanced.py b/Image Occlusion Enhanced.py deleted file mode 100644 index 46ddfa8d..00000000 --- a/Image Occlusion Enhanced.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file imports Image Occlusion Enhanced into Anki -# Please don't edit this if you don't know what you're doing. - -import image_occlusion_enhanced.main diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8f2a235d --- /dev/null +++ b/LICENSE @@ -0,0 +1,699 @@ +This Program is licensed under the GNU Affero General Public License +version 3 ("AGPL"), extended by a number of Additional Terms under +Section 7 of the AGPL. + +If not otherwise noted, this License applies to all files included +with this Program. Files subject to different licensing terms might +also ship with this Program (e.g. the libaddon package), but will +be clearly marked as such in additional LICENSE files +accompanying them. + +The AGPLv3 License and Additional Terms follow. + +============================================================================== + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + +============================================================================== + + ADDITIONAL TERMS APPLICABLE TO THIS PROGRAM + UNDER GNU AGPL VERSION 3 SECTION 7 + +The following additional terms ("Additional Terms") supplement and modify the +GNU Affero General Public License, Version 3 ("AGPL") applicable to the present +Program. + +In addition to the terms and conditions of the AGPL, the present Program is +subject to the further restrictions below: + +1. Trademark and Publicity Rights. + + Except as expressly provided herein, no trademark or publicity rights are + granted. This license does NOT give you any right, title or interest in the + "Glutanimate" name or logo. + + However, Licensees are granted a limited, non-exclusive right to use the + Glutanimate name and logo as part of the preservation of the legal notices + and author attributions within this Program and within Appropriate Legal + Notices displayed by works containing it. + +2. Origin of the Program. + + The origin of the Program must not be misrepresented; you must not claim + that you wrote the original Program. Altered source versions must be plainly + marked as such, and must not be misrepresented as being the original + Program. + +3. Legal Notices and Author Attributions. + + You must reproduce faithfully all trademark, copyright and other proprietary + and legal notices on any copies of the Program or any other required author + attributions. Legal notices or author attributions displayed as part of the + user interface must be preserved as such. + +4. Use of Names of Licensors or Authors for Publicity Purposes. + + Outside of the aforementioned legal notices and author attributions, neither + the name of the copyright holder or its affiliates, any other party who + modifies and/or conveys the Program, nor the names of the Program's + sponsors/supporters/patrons may be used to endorse or promote products + derived from this software without specific prior written permission. + +5. Indemnification. + + IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT OF THAT COVERED + WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK, YOU HEREBY + AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS AND AUTHORS + OF THAT COVERED WORK FOR ANY DAMAGES, DEMANDS, CLAIMS, LOSSES, CAUSES OF + ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION + REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABLITY ARISING FROM, + RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. + +6. Preservation of Licensing Terms. + + Any covered work conveyed by you must include this license text in its + entirety. + +------------------------------------------------------------------------------- + +If you have any questions regarding this license, about any other legal +details, or want to report an infringement of the aforementioned licensing +terms, please feel free to contact me at: diff --git a/README.md b/README.md index 09e3bd3b..0e246130 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,31 @@ -## Image Occlusion Enhanced +

-*Image Occlusion Enhanced* is an add-on for the spaced repetition flashcard app [Anki](http://ankisrs.net/). It allows you to create flashcards that hide parts of an image to test your knowledge of that hidden information. +

Image Occlusion Enhanced for Anki

-### Table of Contents +

+ + + +
+ + + +

- +> Flashcards from images – the easy way + +*Image Occlusion Enhanced* is an add-on for the spaced repetition flashcard app [Anki](https://apps.ankiweb.net/) that allows you to create image-based cloze-deletions. + +### Table of Contents + + - [Screenshots](#screenshots) -- [Changes Compared to Image Occlusion 2.0](#changes-compared-to-image-occlusion-20) +- [Installation](#installation) - [Documentation](#documentation) -- [Known Issues](#known-issues) -- [Credits and License](#credits-and-license) +- [Building](#building) +- [Contributing](#contributing) +- [License and Credits](#license-and-credits) @@ -25,39 +40,53 @@ -### Changes Compared to Image Occlusion 2.0 +### Installation -- **Modify Existing Notes**: Update and modify your IO notes to your heart's content -- **Change Images on the Fly**: Switch to a different image right from the Editor -- **Create Custom Labels**: Your image is lacking a specific label? Now you can create it yourself! -- **Completely Overhauled UI**: Tabs, multi-line entry fields, numerous new hotkey assignments -- **Completely Reworked Note Type**: 4 additional fields, new intuitive field order -- **Updated Card Template**: New card layout, a button to reveal all masks, optimizations for AnkiMobile -- **Full Customization**: Add and rename as many fields as you like -- **Updated Options Interface**: More options, fewer bugs -- **Fully Rewritten Note Generator**: Faster, more extensible, and less bug-prone -- **Performance Improvements**: Smaller memory footprint in general use -- **Stability Improvements**: Bug fixes everywhere +#### AnkiWeb -For more information on recent improvements and additions make sure to check out the changelogs posted on the [Releases page](https://github.com/Glutanimate/image-occlusion-enhanced/releases). +The easiest way to install Image Occlusion Enhanced is through [AnkiWeb](https://ankiweb.net/shared/info/1374772155). + +#### Manual installation + +1. Make sure you have the [latest version](https://apps.ankiweb.net/#download) of Anki 2.1 installed. Earlier releases (e.g. found in various Linux distros) do not support `.ankiaddon` packages. +2. Download the latest `.ankiaddon` package from the [releases tab](https://github.com/glutanimate/image-occlusion-enhanced/releases) (you might need to click on *Assets* below the description to reveal the download links) +3. From Anki's main window, head to *Tools* → *Add-ons* +4. Drag-and-drop the `.ankiaddon` package onto the add-ons list +5. Restart Anki ### Documentation The installation and use of this add-on is detailed in the [Wiki](https://github.com/Glutanimate/image-occlusion-enhanced/wiki) and a [series of video tutorials on YouTube](https://www.youtube.com/playlist?list=PL3MozITKTz5YFHDGB19ypxcYfJ1ITk_6o). More information may also be found in the [AnkiWeb description](docs/description.md). -### Known Issues +### Building + +With [Anki add-on builder](https://github.com/glutanimate/anki-addon-builder/) installed: -*Image Occlusion Enhanced* should now be stable for the most part. However, there still exist a number of longstanding issues that have less to do with the add-on itself and more with Anki and the libraries it's based on. macOS, in particular, has always suffered from compatibility issues with SVG-Edit. + git clone https://github.com/glutanimate/image-occlusion-enhanced.git + cd image-occlusion-enhanced + aab build -For a list of known issues please check out the [Issues](https://github.com/Glutanimate/image-occlusion-enhanced/issues) page. Bug reports and suggestions are always welcome, but it might take me a while to get to them. If you know how to code please feel free to improve this project, file pull requests, etc. +For more information on the build process please refer to [`aab`'s documentation](https://github.com/glutanimate/anki-addon-builder/#usage). -### Credits and License +### Contributing + +Contributions are welcome! Please review the [contribution guidelines](./CONTRIBUTING.md) on how to: + +- Report issues +- File pull requests +- Support the project as a non-developer + +### License and Credits + +*Image Occlusion Enhanced* is *Copyright © 2012-2015 [Tiago Barroso](https://github.com/tmbb)* *Copyright © 2013 [Steve AW](https://github.com/steveaw)* -*Copyright © 2016-2017 [Aristotelis P.](https://glutanimate.com/)* +*Copyright © 2016-2019 [Aristotelis P.](https://glutanimate.com/) (Glutanimate)* + +With code contributions from: Damien Elmes, Kyle Mills, James Kraus, Matt Restko ----- @@ -73,6 +102,12 @@ I would also like to extend my heartfelt thanks to everyone who has helped with - [imagesize.py](https://github.com/shibukawa/imagesize_py) v0.7.1. Copyright (c) 2016 Yoshiki Shibukawa. Licensed under the MIT license. -This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +Image Occlusion Enhanced is free and open-source software. The add-on code that runs within Anki is released under the GNU AGPLv3 license, extended by a number of additional terms. For more information please see the [LICENSE](https://github.com/glutanimate/image-occlusion-enhanced/blob/master/LICENSE) file that accompanied this program. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. \ No newline at end of file +---- + +
The continued development of this add-on is made possible
thanks to my Patreon and Ko-Fi supporters. +
You guys rock ❤️ !
+
\ No newline at end of file diff --git a/addon.json b/addon.json new file mode 100644 index 00000000..c875966d --- /dev/null +++ b/addon.json @@ -0,0 +1,15 @@ +{ + "display_name": "Image Occlusion Enhanced", + "module_name": "image_occlusion_enhanced", + "repo_name": "image-occlusion-enhanced", + "ankiweb_id": "1374772155", + "author": "Glutanimate", + "contact": "https://glutanimate.com", + "homepage": "https://github.com/glutanimate/image-occlusion-enhanced", + "copyright_start": 2016, + "tags": "image-occlusion editor", + "conflicts": [], + "targets": [ + "anki21" + ] +} \ No newline at end of file diff --git a/build_zip.sh b/build_zip.sh deleted file mode 100755 index 03af09cd..00000000 --- a/build_zip.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# builds zip file for Ankiweb - -latestTag=$(git describe HEAD --tags --abbrev=0) -outFile="image-occlusion-enhanced-$latestTag.zip" - -git archive --format zip --output "$outFile" "$latestTag" \ No newline at end of file diff --git a/docs/description-anki20.md b/docs/description-anki20.md new file mode 100644 index 00000000..a00d0150 --- /dev/null +++ b/docs/description-anki20.md @@ -0,0 +1,113 @@ +*Image Occlusion Enhanced* is an updated version of *Image Occlusion 2.0*. + +Image Occlusion lets you create cards that **hide parts of an image** to test your knowledge of that hidden information. The cards generated by this add-on would be best described as **image-based cloze-deletions**: + +![](https://raw.githubusercontent.com/glutanimate/image-occlusion-enhanced/master/screenshots/ankiweb.png) + +**COMPATIBILITY** + +This version of Image Occlusion Enhanced only works with Anki's stable release branch (Anki 2.0.x, version 2.0.30 and up). If you are using Anki 2.1, make sure to check out the latest [development release of the add-on](https://ankiweb.net/shared/info/1374772155). + +**CHANGES COMPARED TO IO 2.0** + +- **Modify Existing Notes**: Update and modify your IO notes to your heart's content +- **Change Images on the Fly**: Switch to a different image right from the Editor +- **Create Custom Labels**: Your image is lacking a specific label? Now you can create it yourself! +- **Completely Overhauled UI**: Tabs, multi-line entry fields, numerous new hotkey assignments +- **Completely Reworked Note Type**: 4 additional fields, new intuitive field order +- **Updated Card Template**: New card layout, a button to reveal all masks, optimizations for AnkiMobile +- **Full Customization**: Add and rename as many fields as you like +- **Updated Options Interface**: More options, fewer bugs +- **Fully Rewritten Note Generator**: Faster, more extensible, and less bug-prone +- **Performance Improvements**: Smaller memory footprint in general use +- **Stability Improvements**: Bug fixes everywhere + +[Here's a great overview](https://www.youtube.com/watch?v=7pU9L0XybyI) by *Med School Insiders* on YouTube that goes over the key differences compared to older versions of IO. + +**RELEASE LOG** + +This section will always be updated with the latest changes to *Image Occlusion Enhanced*. More details on major releases are listed in the next section. For a full release log, including smaller bug fix releases and more details on each entry, make sure to check out the [releases page on GitHub](https://github.com/Glutanimate/image-occlusion-enhanced/releases). + +2017-04-17: **v1.2.2** - Fixes a few smaller issues +2017-02-14: **v1.2.0** - Usability improvements and under-the-hood changes +2016-01-14: **v1.1.0** - New features, usability improvements, and bug fixes +2016-11-10: **v1.0.0** - Ability to modify existing notes, among many other new features +2016-09-28: **v0.3.0** - A large number of improvements to the Masks Editor +2016-04-03: **v0.2.0** - Initial public release + +**LATEST CHANGES AND NEWS** + +*2018-06-30* + +An alpha version of Image Occlusion Enhanced for Anki 2.1 is now available [here](https://ankiweb.net/shared/info/1374772155). + +*2017-04-17* + +A recent iOS-update might be affecting Image Occlusion 2.0 cards that were created earlier than 2015. If your occlusion masks are not drawn properly please use [this supplementary](https://ankiweb.net/shared/info/1048682320) add-on to fix them (thanks to mrestko for coming up with this solution). + +**v1.2.2** + +- Fixed: rare AttributeError when changing the image (thanks to Renita and Joshua for the report) +- Fixed: GIFs should be supported on Windows and macOS now (thanks to Tiago for the help) +- Fixed: incompatibility with upcoming release of "Quick note and deck buttons" add-on (thanks to drroscoe for the reminder) + +**INSTALLATION** + +If you are starting from a fresh Anki installation you can simply follow the generic instructions listed below this add-on description. + +**UPDATING** + +Repeating the installation procedure will allow you to update to the latest release of the add-on. However, if you are upgrading from Image Occlusion 2.0 or early versions of Image Occlusion Enhanced please make sure to read [the Wiki section on upgrading](https://github.com/Glutanimate/image-occlusion-enhanced/wiki/Upgrading) first. + +**USAGE** + +The use of this add-on is detailed in the [Wiki](https://github.com/Glutanimate/image-occlusion-enhanced/wiki) and a [series of video tutorials on YouTube](https://www.youtube.com/playlist?list=PL3MozITKTz5YFHDGB19ypxcYfJ1ITk_6o). + +**HELP** + +Please check out the [Troubleshooting section](https://github.com/Glutanimate/image-occlusion-enhanced/wiki/Troubleshooting) in the Wiki if you are experiencing issues with the add-on. + +Bug reports and suggestions are always welcome, but it might take me a while to get to them. **Please do not post bug reports in the review section**, as I will not be able to help you or reply. Instead, please use the [official support thread](https://anki.tenderapp.com/discussions/add-ons/8295-image-occlusion-enhanced-official-thread) (no registration necessary!). + +**CREDITS AND LICENSE** + +*Copyright © 2012-2015 [Tiago Barroso](https://github.com/tmbb)* +*Copyright © 2013 [Steve AW](https://github.com/steveaw)* +*Copyright © 2016-2017 [Aristotelis P.](https://glutanimate.com/)* + +*Image Occlusion Enhanced* is based on [Image Occlusion 2.0](https://github.com/tmbb/image-occlusion-2) by Tiago Barroso and [Simple Picture Occlusion](https://github.com/steveaw/anki_addons) by Steve AW. All credit for the original add-ons goes to their respective authors. *Image Occlusion Enhanced* would not exist without their work. + +I would also like to extend my heartfelt thanks to everyone who has helped with testing, provided suggestions, or contributed in any other way. + +Licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl.html). The code for this add-on is available on [![GitHub icon](https://glutanimate.com/logos/github.svg) GitHub](https://github.com/glutanimate/image-occlusion-enhanced). For more information on the licensing terms and other software shipped with this package please check out the [README](https://github.com/Glutanimate/image-occlusion-enhanced#credits). + +**OTHER ADD-ONS** + +Make sure to also give some of my other add-ons a try: + +- [Cloze Overlapper](https://ankiweb.net/shared/info/969733775) - memorize lists and enumerations effectively +- [Review Heatmap](https://ankiweb.net/shared/info/1771074083) - tracks your review streaks and motivates you to keep on going +- [Advanced Previewer](https://ankiweb.net/shared/info/544521385) - preview and review multiple cards at once right from within the previewer +- [Search and Replace Tags](https://ankiweb.net/shared/info/138501288) - rename tags across multiple notes +- [Batch Note Editing](https://ankiweb.net/shared/info/291119185) - makes batch-editing notes a breeze +- [Sticky Searches](https://ankiweb.net/shared/info/594622823) - quick toggles for often used search terms in the browser + +**ADD-ON COMMISSIONS** + +A lot of my add-ons were commissioned by fellow Anki users. If you enjoy my work and would like to hire my services to work on an add-on or new feature, please feel free to reach out to me at: ![Email icon](https://glutanimate.com/logos/email.svg) ankiglutanimate [αt] gmail . com + +**MORE RESOURCES** + +Want to stay up-to-date with my latest add-on releases and updates? Feel free to follow me on Twitter: [![Twitter bird](https://glutanimate.com/logos/twitter.svg)@Glutanimate](https://twitter.com/glutanimate) + +New to Anki? Make sure to check out my YouTube channel where I post weekly tutorials on Anki add-ons and related topics: [![YouTube playbutton](https://glutanimate.com/logos/youtube.svg) / Glutanimate](https://www.youtube.com/c/glutanimate) + +============================================ + +**SUPPORT THIS ADD-ON** + +Writing, supporting, and maintaining Anki add-ons like these takes a lot of time and effort. If *Image Occlusion Enhanced* has been a valuable asset in your studies, please consider using one of the buttons below to support my efforts by buying me a **coffee**, **sandwich**, **meal**, or anything else you'd like: + +![](https://glutanimate.com/logos/paypal.svg) [![](https://glutanimate.com/logos/contrib_btnsw_coffee.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4FT9NG3NJMY4U&on0=Project&os0=image-occlusion "Buy me a coffee ☺") [![](https://glutanimate.com/logos/contrib_btnsw_sandwich.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YKSP7QF45Y7SJ&on0=Project&os0=image-occlusion "Buy me a burger 😊") [![](https://glutanimate.com/logos/contrib_btnsw_meal.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MVDM6JAL2R5JA&on0=Project&os0=image-occlusion "Buy me a meal 😄") [![](https://glutanimate.com/logos/contrib_btnsw_custom.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=EYNV4ECSKBGE4&on0=Project&os0=image-occlusion "Contribute a custom amount ☺") + +Each and every contribution is greatly appreciated and will help me maintain and improve *Image Occlusion Enhanced* as time goes by! diff --git a/docs/description.md b/docs/description.md index 9f22f646..03720bae 100644 --- a/docs/description.md +++ b/docs/description.md @@ -1,109 +1,49 @@ -*Image Occlusion Enhanced* is an updated version of *Image Occlusion 2.0*. + Image Occlusion lets you create cards that **hide parts of an image** to test your knowledge of that hidden information. The cards generated by this add-on would be best described as **image-based cloze-deletions**: ![](https://raw.githubusercontent.com/glutanimate/image-occlusion-enhanced/master/screenshots/ankiweb.png) -**COMPATIBILITY** +### IMPORTANT -This add-on only works with Anki's stable release branch (2.0.x ≥ 2.0.30). The 2.1 beta branch is **not supported** at this point in time. +This is a test release of Image Occlusion Enhanced for Anki 2.1. Please make sure to carefully read through this description and familiarize yourself with the official Anki 2.1 change notes notes before giving the add-on a try. -**CHANGES COMPARED TO IO 2.0** +The present AnkiWeb upload is temporary and will be removed in favor of the [regular add-on listing](https://ankiweb.net/shared/info/1111933094) once testing is complete. -- **Modify Existing Notes**: Update and modify your IO notes to your heart's content -- **Change Images on the Fly**: Switch to a different image right from the Editor -- **Create Custom Labels**: Your image is lacking a specific label? Now you can create it yourself! -- **Completely Overhauled UI**: Tabs, multi-line entry fields, numerous new hotkey assignments -- **Completely Reworked Note Type**: 4 additional fields, new intuitive field order -- **Updated Card Template**: New card layout, a button to reveal all masks, optimizations for AnkiMobile -- **Full Customization**: Add and rename as many fields as you like -- **Updated Options Interface**: More options, fewer bugs -- **Fully Rewritten Note Generator**: Faster, more extensible, and less bug-prone -- **Performance Improvements**: Smaller memory footprint in general use -- **Stability Improvements**: Bug fixes everywhere + -[Here's a great overview](https://www.youtube.com/watch?v=7pU9L0XybyI) by *Med School Insiders* on YouTube that goes over the key differences compared to older versions of IO. +### LIMITATIONS OF THIS ALPHA RELEASE -**RELEASE LOG** +There are a number of known limitations to this alpha release that you need to be aware of: -This section will always be updated with the latest changes to *Image Occlusion Enhanced*. More details on major releases are listed in the next section. For a full release log, including smaller bug fix releases and more details on each entry, make sure to check out the [releases page on GitHub](https://github.com/Glutanimate/image-occlusion-enhanced/releases). +- Due to compatibility issues between SVG-Edit and the new Chromium renderer in Anki 2.1, some of the features in the masks editor no longer work correctly. My hope is to address these in the following beta release: + + [Pointer not changing to selection mode when clicking on shape](https://github.com/glutanimate/image-occlusion-enhanced/issues/54) + + [Path tool no longer working](https://github.com/glutanimate/image-occlusion-enhanced/issues/56) + + [https://github.com/glutanimate/image-occlusion-enhanced/issues/57](https://github.com/glutanimate/image-occlusion-enhanced/issues/57) +- Please do not invoke the add-on's settings menu while the I/O Editor is running. There is currently no support for updating I/O editing sessions at runtime, and while most settings will simply only not be applied, others might cause the add-on to stop working correctly until the editor session is restarted. The same applies to modifications to the add-on's note type via Anki's built-in note type manager. -2017-04-17: **v1.2.2** - Fixes a few smaller issues -2017-02-14: **v1.2.0** - Usability improvements and under-the-hood changes -2016-01-14: **v1.1.0** - New features, usability improvements, and bug fixes -2016-11-10: **v1.0.0** - Ability to modify existing notes, among many other new features -2016-09-28: **v0.3.0** - A large number of improvements to the Masks Editor -2016-04-03: **v0.2.0** - Initial public release +Of course there also bound to be some unforeseen bugs and regressions in the alpha. If you experience any of these please make sure to either report them on the add-on's [bug tracker](https://github.com/glutanimate/image-occlusion-enhanced/issues) or in the [official support thread](https://anki.tenderapp.com/discussions/add-ons/8295-image-occlusion-enhanced-official-thread). Any and all feedback is appreciated! -**LATEST CHANGES AND NEWS** +### USAGE -*2017-04-17* +The use of this add-on is detailed in the [Wiki](https://github.com/Glutanimate/image-occlusion-enhanced/wiki) and a [series of video tutorials on YouTube](https://www.youtube.com/playlist?list=PL3MozITKTz5YFHDGB19ypxcYfJ1ITk_6o). Please note that most of these resources have yet to be updated for Anki 2.1, and as such might not cover recent releases of the add-on fully. -A recent iOS-update might be affecting Image Occlusion 2.0 cards that were created earlier than 2015. If your occlusion masks are not drawn properly please use [this supplementary](https://ankiweb.net/shared/info/1048682320) add-on to fix them (thanks to mrestko for coming up with this solution). + -**v1.2.2** - -- Fixed: rare AttributeError when changing the image (thanks to Renita and Joshua for the report) -- Fixed: GIFs should be supported on Windows and macOS now (thanks to Tiago for the help) -- Fixed: incompatibility with upcoming release of "Quick note and deck buttons" add-on (thanks to drroscoe for the reminder) - -**INSTALLATION** - -If you are starting from a fresh Anki installation you can simply follow the generic instructions listed below this add-on description. - -**UPDATING** - -Repeating the installation procedure will allow you to update to the latest release of the add-on. However, if you are upgrading from Image Occlusion 2.0 or early versions of Image Occlusion Enhanced please make sure to read [the Wiki section on upgrading](https://github.com/Glutanimate/image-occlusion-enhanced/wiki/Upgrading) first. - -**USAGE** - -The use of this add-on is detailed in the [Wiki](https://github.com/Glutanimate/image-occlusion-enhanced/wiki) and a [series of video tutorials on YouTube](https://www.youtube.com/playlist?list=PL3MozITKTz5YFHDGB19ypxcYfJ1ITk_6o). - -**HELP** - -Please check out the [Troubleshooting section](https://github.com/Glutanimate/image-occlusion-enhanced/wiki/Troubleshooting) in the Wiki if you are experiencing issues with the add-on. - -Bug reports and suggestions are always welcome, but it might take me a while to get to them. **Please do not post bug reports in the review section**, as I will not be able to help you or reply. Instead, please use the [official support thread](https://anki.tenderapp.com/discussions/add-ons/8295-image-occlusion-enhanced-official-thread) (no registration necessary!). - -**CREDITS AND LICENSE** +### CREDITS AND LICENSE *Copyright © 2012-2015 [Tiago Barroso](https://github.com/tmbb)* *Copyright © 2013 [Steve AW](https://github.com/steveaw)* -*Copyright © 2016-2017 [Aristotelis P.](https://glutanimate.com/)* +*Copyright © 2016-2019 [Aristotelis P.](https://glutanimate.com/) (Glutanimate)* + +With code contributions from: Damien Elmes, Kyle Mills, James Kraus, Matt Restko *Image Occlusion Enhanced* is based on [Image Occlusion 2.0](https://github.com/tmbb/image-occlusion-2) by Tiago Barroso and [Simple Picture Occlusion](https://github.com/steveaw/anki_addons) by Steve AW. All credit for the original add-ons goes to their respective authors. *Image Occlusion Enhanced* would not exist without their work. I would also like to extend my heartfelt thanks to everyone who has helped with testing, provided suggestions, or contributed in any other way. -Licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl.html). The code for this add-on is available on [![GitHub icon](https://glutanimate.com/logos/github.svg) GitHub](https://github.com/glutanimate/image-occlusion-enhanced). For more information on the licensing terms and other software shipped with this package please check out the [README](https://github.com/Glutanimate/image-occlusion-enhanced#credits). - -**OTHER ADD-ONS** - -Make sure to also give some of my other add-ons a try: - -- [Cloze Overlapper](https://ankiweb.net/shared/info/969733775) - memorize lists and enumerations effectively -- [Review Heatmap](https://ankiweb.net/shared/info/1771074083) - tracks your review streaks and motivates you to keep on going -- [Advanced Previewer](https://ankiweb.net/shared/info/544521385) - preview and review multiple cards at once right from within the previewer -- [Search and Replace Tags](https://ankiweb.net/shared/info/138501288) - rename tags across multiple notes -- [Batch Note Editing](https://ankiweb.net/shared/info/291119185) - makes batch-editing notes a breeze -- [Sticky Searches](https://ankiweb.net/shared/info/594622823) - quick toggles for often used search terms in the browser - -**ADD-ON COMMISSIONS** - -A lot of my add-ons were commissioned by fellow Anki users. If you enjoy my work and would like to hire my services to work on an add-on or new feature, please feel free to reach out to me at: ![Email icon](https://glutanimate.com/logos/email.svg) ankiglutanimate [αt] gmail . com - -**MORE RESOURCES** - -Want to stay up-to-date with my latest add-on releases and updates? Feel free to follow me on Twitter: [![Twitter bird](https://glutanimate.com/logos/twitter.svg)@Glutanimate](https://twitter.com/glutanimate) - -New to Anki? Make sure to check out my YouTube channel where I post weekly tutorials on Anki add-ons and related topics: [![YouTube playbutton](https://glutanimate.com/logos/youtube.svg) / Glutanimate](https://www.youtube.com/c/glutanimate) - -============================================ - -**SUPPORT THIS ADD-ON** - -Writing, supporting, and maintaining Anki add-ons like these takes a lot of time and effort. If *Image Occlusion Enhanced* has been a valuable asset in your studies, please consider using one of the buttons below to support my efforts by buying me a **coffee**, **sandwich**, **meal**, or anything else you'd like: +Licensed under the _GNU AGPLv3_, extended by a number of additional terms. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. For more information on the license please see the [LICENSE file](https://github.com/glutanimate/image-occlusion-enhanced/blob/master/LICENSE) accompanying this add-on. The source code is available on [![GitHub icon](https://glutanimate.com/logos/github.svg) GitHub](https://github.com/glutanimate/image-occlusion-enhanced). Pull requests and other contributions are welcome! -![](https://glutanimate.com/logos/paypal.svg) [![](https://glutanimate.com/logos/contrib_btnsw_coffee.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4FT9NG3NJMY4U&on0=Project&os0=image-occlusion "Buy me a coffee ☺") [![](https://glutanimate.com/logos/contrib_btnsw_sandwich.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YKSP7QF45Y7SJ&on0=Project&os0=image-occlusion "Buy me a burger 😊") [![](https://glutanimate.com/logos/contrib_btnsw_meal.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MVDM6JAL2R5JA&on0=Project&os0=image-occlusion "Buy me a meal 😄") [![](https://glutanimate.com/logos/contrib_btnsw_custom.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=EYNV4ECSKBGE4&on0=Project&os0=image-occlusion "Contribute a custom amount ☺") + -Each and every contribution is greatly appreciated and will help me maintain and improve *Image Occlusion Enhanced* as time goes by! + diff --git a/docs/help.md b/docs/help.md new file mode 100644 index 00000000..3b451d68 --- /dev/null +++ b/docs/help.md @@ -0,0 +1,27 @@ +*Basic Instructions* + +1. With the rectangle tool or any other shape tool selected, draw over your labels +2. (Optional): Fill out additional information about your cards by switching to the *Fields* tab +3. Click on one of the *Add Cards* buttons at the bottom of the window to add the cards to your collection + +*Drawing Custom Labels* + +1. Draw up the layers sidepanel by clicking on the *Layers* button at the right edge of the editor +2. Switch to the *Labels* layer by left-clicking on it. You can also switch to the labels layer directly by using Ctrl + Shift + L. +3. Anything you draw in this layer – be it text, lines, or shapes – will appear above the image, but still below your masks. All of the painting tools in the left sidebar are at your disposal. +4. Switch back to the masks layer, either via the *Layers* sidepanel, or by using the Ctrl + Shift + M hotkey. + +*Grouping Shapes* + +1. Select multiple shapes, either by drawing a selection rectangle with the selection tool active (S), or by shift-clicking on multiple shapes +2. Either use the G hotkey or the *Group Elements* tools in the top-bar to group your items + +Grouped shapes will form a single card. + +*More Information* + +For more information please refer to the following resources: + +- [Image Occlusion Enhanced Wiki]() +- [YouTube Tutorials]() +- [Official support thread]() \ No newline at end of file diff --git a/image_occlusion_enhanced/Imaging/PIL.pth b/image_occlusion_enhanced/Imaging/PIL.pth deleted file mode 100644 index b338169c..00000000 --- a/image_occlusion_enhanced/Imaging/PIL.pth +++ /dev/null @@ -1 +0,0 @@ -PIL diff --git a/image_occlusion_enhanced/Imaging/PIL/BmpImagePlugin.py b/image_occlusion_enhanced/Imaging/PIL/BmpImagePlugin.py deleted file mode 100644 index 09c0a214..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/BmpImagePlugin.py +++ /dev/null @@ -1,251 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# BMP file handler -# -# Windows (and OS/2) native bitmap storage format. -# -# history: -# 1995-09-01 fl Created -# 1996-04-30 fl Added save -# 1997-08-27 fl Fixed save of 1-bit images -# 1998-03-06 fl Load P images as L where possible -# 1998-07-03 fl Load P images as 1 where possible -# 1998-12-29 fl Handle small palettes -# 2002-12-30 fl Fixed load of 1-bit palette images -# 2003-04-21 fl Fixed load of 1-bit monochrome images -# 2003-04-23 fl Added limited support for BI_BITFIELDS compression -# -# Copyright (c) 1997-2003 by Secret Labs AB -# Copyright (c) 1995-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - - -__version__ = "0.7" - - -import string -import Image, ImageFile, ImagePalette - - -# -# -------------------------------------------------------------------- -# Read BMP file - -def i16(c): - return ord(c[0]) + (ord(c[1])<<8) - -def i32(c): - return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) - - -BIT2MODE = { - # bits => mode, rawmode - 1: ("P", "P;1"), - 4: ("P", "P;4"), - 8: ("P", "P"), - 16: ("RGB", "BGR;15"), - 24: ("RGB", "BGR"), - 32: ("RGB", "BGRX") -} - -def _accept(prefix): - return prefix[:2] == "BM" - -## -# Image plugin for the Windows BMP format. - -class BmpImageFile(ImageFile.ImageFile): - - format = "BMP" - format_description = "Windows Bitmap" - - def _bitmap(self, header = 0, offset = 0): - - if header: - self.fp.seek(header) - - read = self.fp.read - - # CORE/INFO - s = read(4) - s = s + ImageFile._safe_read(self.fp, i32(s)-4) - - if len(s) == 12: - - # OS/2 1.0 CORE - bits = i16(s[10:]) - self.size = i16(s[4:]), i16(s[6:]) - compression = 0 - lutsize = 3 - colors = 0 - direction = -1 - - elif len(s) in [40, 64]: - - # WIN 3.1 or OS/2 2.0 INFO - bits = i16(s[14:]) - self.size = i32(s[4:]), i32(s[8:]) - compression = i32(s[16:]) - lutsize = 4 - colors = i32(s[32:]) - direction = -1 - if s[11] == '\xff': - # upside-down storage - self.size = self.size[0], 2**32 - self.size[1] - direction = 0 - - else: - raise IOError("Unsupported BMP header type (%d)" % len(s)) - - if not colors: - colors = 1 << bits - - # MODE - try: - self.mode, rawmode = BIT2MODE[bits] - except KeyError: - raise IOError("Unsupported BMP pixel depth (%d)" % bits) - - if compression == 3: - # BI_BITFIELDS compression - mask = i32(read(4)), i32(read(4)), i32(read(4)) - if bits == 32 and mask == (0xff0000, 0x00ff00, 0x0000ff): - rawmode = "BGRX" - elif bits == 16 and mask == (0x00f800, 0x0007e0, 0x00001f): - rawmode = "BGR;16" - elif bits == 16 and mask == (0x007c00, 0x0003e0, 0x00001f): - rawmode = "BGR;15" - else: - # print bits, map(hex, mask) - raise IOError("Unsupported BMP bitfields layout") - elif compression != 0: - raise IOError("Unsupported BMP compression (%d)" % compression) - - # LUT - if self.mode == "P": - palette = [] - greyscale = 1 - if colors == 2: - indices = (0, 255) - else: - indices = range(colors) - for i in indices: - rgb = read(lutsize)[:3] - if rgb != chr(i)*3: - greyscale = 0 - palette.append(rgb) - if greyscale: - if colors == 2: - self.mode = rawmode = "1" - else: - self.mode = rawmode = "L" - else: - self.mode = "P" - self.palette = ImagePalette.raw( - "BGR", string.join(palette, "") - ) - - if not offset: - offset = self.fp.tell() - - self.tile = [("raw", - (0, 0) + self.size, - offset, - (rawmode, ((self.size[0]*bits+31)>>3)&(~3), direction))] - - self.info["compression"] = compression - - def _open(self): - - # HEAD - s = self.fp.read(14) - if s[:2] != "BM": - raise SyntaxError("Not a BMP file") - offset = i32(s[10:]) - - self._bitmap(offset=offset) - - -class DibImageFile(BmpImageFile): - - format = "DIB" - format_description = "Windows Bitmap" - - def _open(self): - self._bitmap() - -# -# -------------------------------------------------------------------- -# Write BMP file - -def o16(i): - return chr(i&255) + chr(i>>8&255) - -def o32(i): - return chr(i&255) + chr(i>>8&255) + chr(i>>16&255) + chr(i>>24&255) - -SAVE = { - "1": ("1", 1, 2), - "L": ("L", 8, 256), - "P": ("P", 8, 256), - "RGB": ("BGR", 24, 0), -} - -def _save(im, fp, filename, check=0): - - try: - rawmode, bits, colors = SAVE[im.mode] - except KeyError: - raise IOError("cannot write mode %s as BMP" % im.mode) - - if check: - return check - - stride = ((im.size[0]*bits+7)/8+3)&(~3) - header = 40 # or 64 for OS/2 version 2 - offset = 14 + header + colors * 4 - image = stride * im.size[1] - - # bitmap header - fp.write("BM" + # file type (magic) - o32(offset+image) + # file size - o32(0) + # reserved - o32(offset)) # image data offset - - # bitmap info header - fp.write(o32(header) + # info header size - o32(im.size[0]) + # width - o32(im.size[1]) + # height - o16(1) + # planes - o16(bits) + # depth - o32(0) + # compression (0=uncompressed) - o32(image) + # size of bitmap - o32(1) + o32(1) + # resolution - o32(colors) + # colors used - o32(colors)) # colors important - - fp.write("\000" * (header - 40)) # padding (for OS/2 format) - - if im.mode == "1": - for i in (0, 255): - fp.write(chr(i) * 4) - elif im.mode == "L": - for i in range(256): - fp.write(chr(i) * 4) - elif im.mode == "P": - fp.write(im.im.getpalette("RGB", "BGRX")) - - ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, stride, -1))]) - -# -# -------------------------------------------------------------------- -# Registry - -Image.register_open(BmpImageFile.format, BmpImageFile, _accept) -Image.register_save(BmpImageFile.format, _save) - -Image.register_extension(BmpImageFile.format, ".bmp") diff --git a/image_occlusion_enhanced/Imaging/PIL/GifImagePlugin.py b/image_occlusion_enhanced/Imaging/PIL/GifImagePlugin.py deleted file mode 100644 index 4d03493b..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/GifImagePlugin.py +++ /dev/null @@ -1,407 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# GIF file handling -# -# History: -# 1995-09-01 fl Created -# 1996-12-14 fl Added interlace support -# 1996-12-30 fl Added animation support -# 1997-01-05 fl Added write support, fixed local colour map bug -# 1997-02-23 fl Make sure to load raster data in getdata() -# 1997-07-05 fl Support external decoder (0.4) -# 1998-07-09 fl Handle all modes when saving (0.5) -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6) -# 2001-04-17 fl Added palette optimization (0.7) -# 2002-06-06 fl Added transparency support for save (0.8) -# 2004-02-24 fl Disable interlacing for small images -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1995-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - - -__version__ = "0.9" - - -import Image, ImageFile, ImagePalette - - -# -------------------------------------------------------------------- -# Helpers - -def i16(c): - return ord(c[0]) + (ord(c[1])<<8) - -def o16(i): - return chr(i&255) + chr(i>>8&255) - - -# -------------------------------------------------------------------- -# Identify/read GIF files - -def _accept(prefix): - return prefix[:6] in ["GIF87a", "GIF89a"] - -## -# Image plugin for GIF images. This plugin supports both GIF87 and -# GIF89 images. - -class GifImageFile(ImageFile.ImageFile): - - format = "GIF" - format_description = "Compuserve GIF" - - global_palette = None - - def data(self): - s = self.fp.read(1) - if s and ord(s): - return self.fp.read(ord(s)) - return None - - def _open(self): - - # Screen - s = self.fp.read(13) - if s[:6] not in ["GIF87a", "GIF89a"]: - raise SyntaxError, "not a GIF file" - - self.info["version"] = s[:6] - - self.size = i16(s[6:]), i16(s[8:]) - - self.tile = [] - - flags = ord(s[10]) - - bits = (flags & 7) + 1 - - if flags & 128: - # get global palette - self.info["background"] = ord(s[11]) - # check if palette contains colour indices - p = self.fp.read(3<= 3 and ord(block[0]) == 1: - self.info["loop"] = i16(block[1:3]) - while self.data(): - pass - - elif s == ",": - # - # local image - # - s = self.fp.read(9) - - # extent - x0, y0 = i16(s[0:]), i16(s[2:]) - x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:]) - flags = ord(s[8]) - - interlace = (flags & 64) != 0 - - if flags & 128: - bits = (flags & 7) + 1 - self.palette =\ - ImagePalette.raw("RGB", self.fp.read(3<%s" % (file, filename)) - else: - os.system("ppmquant 256 %s | ppmtogif >%s" % (file, filename)) - try: os.unlink(file) - except: pass - - -# -------------------------------------------------------------------- -# GIF utilities - -def getheader(im, info=None): - """Return a list of strings representing a GIF header""" - - optimize = info and info.get("optimize", 0) - - s = [ - "GIF87a" + # magic - o16(im.size[0]) + # size - o16(im.size[1]) + - chr(7 + 128) + # flags: bits + palette - chr(0) + # background - chr(0) # reserved/aspect - ] - - if optimize: - # minimize color palette - i = 0 - maxcolor = 0 - for count in im.histogram(): - if count: - maxcolor = i - i = i + 1 - else: - maxcolor = 256 - - # global palette - if im.mode == "P": - # colour palette - s.append(im.im.getpalette("RGB")[:maxcolor*3]) - else: - # greyscale - for i in range(maxcolor): - s.append(chr(i) * 3) - - return s - -def getdata(im, offset = (0, 0), **params): - """Return a list of strings representing this image. - The first string is a local image header, the rest contains - encoded image data.""" - - class collector: - data = [] - def write(self, data): - self.data.append(data) - - im.load() # make sure raster data is available - - fp = collector() - - try: - im.encoderinfo = params - - # local image header - fp.write("," + - o16(offset[0]) + # offset - o16(offset[1]) + - o16(im.size[0]) + # size - o16(im.size[1]) + - chr(0) + # flags - chr(8)) # bits - - ImageFile._save(im, fp, [("gif", (0,0)+im.size, 0, RAWMODE[im.mode])]) - - fp.write("\0") # end of image data - - finally: - del im.encoderinfo - - return fp.data - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(GifImageFile.format, GifImageFile, _accept) -Image.register_save(GifImageFile.format, _save) -Image.register_extension(GifImageFile.format, ".gif") -Image.register_mime(GifImageFile.format, "image/gif") - -# -# Uncomment the following line if you wish to use NETPBM/PBMPLUS -# instead of the built-in "uncompressed" GIF encoder - -# Image.register_save(GifImageFile.format, _save_netpbm) diff --git a/image_occlusion_enhanced/Imaging/PIL/Image.py b/image_occlusion_enhanced/Imaging/PIL/Image.py deleted file mode 100644 index 77a532af..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/Image.py +++ /dev/null @@ -1,2127 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# the Image class wrapper -# -# partial release history: -# 1995-09-09 fl Created -# 1996-03-11 fl PIL release 0.0 (proof of concept) -# 1996-04-30 fl PIL release 0.1b1 -# 1999-07-28 fl PIL release 1.0 final -# 2000-06-07 fl PIL release 1.1 -# 2000-10-20 fl PIL release 1.1.1 -# 2001-05-07 fl PIL release 1.1.2 -# 2002-03-15 fl PIL release 1.1.3 -# 2003-05-10 fl PIL release 1.1.4 -# 2005-03-28 fl PIL release 1.1.5 -# 2006-12-02 fl PIL release 1.1.6 -# 2009-11-15 fl PIL release 1.1.7 -# -# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2009 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -VERSION = "1.1.7" - -try: - import warnings -except ImportError: - warnings = None - -class _imaging_not_installed: - # module placeholder - def __getattr__(self, id): - raise ImportError("The _imaging C module is not installed") - -try: - # give Tk a chance to set up the environment, in case we're - # using an _imaging module linked against libtcl/libtk (use - # __import__ to hide this from naive packagers; we don't really - # depend on Tk unless ImageTk is used, and that module already - # imports Tkinter) - __import__("FixTk") -except ImportError: - pass - -try: - # If the _imaging C module is not present, you can still use - # the "open" function to identify files, but you cannot load - # them. Note that other modules should not refer to _imaging - # directly; import Image and use the Image.core variable instead. - import _imaging - core = _imaging - del _imaging -except ImportError, v: - core = _imaging_not_installed() - if str(v)[:20] == "Module use of python" and warnings: - # The _imaging C module is present, but not compiled for - # the right version (windows only). Print a warning, if - # possible. - warnings.warn( - "The _imaging extension was built for another version " - "of Python; most PIL functions will be disabled", - RuntimeWarning - ) - -import ImageMode -import ImagePalette - -import os, string, sys - -# type stuff -from types import IntType, StringType, TupleType - -try: - UnicodeStringType = type(unicode("")) - ## - # (Internal) Checks if an object is a string. If the current - # Python version supports Unicode, this checks for both 8-bit - # and Unicode strings. - def isStringType(t): - return isinstance(t, StringType) or isinstance(t, UnicodeStringType) -except NameError: - def isStringType(t): - return isinstance(t, StringType) - -## -# (Internal) Checks if an object is a tuple. - -def isTupleType(t): - return isinstance(t, TupleType) - -## -# (Internal) Checks if an object is an image object. - -def isImageType(t): - return hasattr(t, "im") - -## -# (Internal) Checks if an object is a string, and that it points to a -# directory. - -def isDirectory(f): - return isStringType(f) and os.path.isdir(f) - -from operator import isNumberType, isSequenceType - -# -# Debug level - -DEBUG = 0 - -# -# Constants (also defined in _imagingmodule.c!) - -NONE = 0 - -# transpose -FLIP_LEFT_RIGHT = 0 -FLIP_TOP_BOTTOM = 1 -ROTATE_90 = 2 -ROTATE_180 = 3 -ROTATE_270 = 4 - -# transforms -AFFINE = 0 -EXTENT = 1 -PERSPECTIVE = 2 -QUAD = 3 -MESH = 4 - -# resampling filters -NONE = 0 -NEAREST = 0 -ANTIALIAS = 1 # 3-lobed lanczos -LINEAR = BILINEAR = 2 -CUBIC = BICUBIC = 3 - -# dithers -NONE = 0 -NEAREST = 0 -ORDERED = 1 # Not yet implemented -RASTERIZE = 2 # Not yet implemented -FLOYDSTEINBERG = 3 # default - -# palettes/quantizers -WEB = 0 -ADAPTIVE = 1 - -# categories -NORMAL = 0 -SEQUENCE = 1 -CONTAINER = 2 - -# -------------------------------------------------------------------- -# Registries - -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -EXTENSION = {} - -# -------------------------------------------------------------------- -# Modes supported by this version - -_MODEINFO = { - # NOTE: this table will be removed in future versions. use - # getmode* functions or ImageMode descriptors instead. - - # official modes - "1": ("L", "L", ("1",)), - "L": ("L", "L", ("L",)), - "I": ("L", "I", ("I",)), - "F": ("L", "F", ("F",)), - "P": ("RGB", "L", ("P",)), - "RGB": ("RGB", "L", ("R", "G", "B")), - "RGBX": ("RGB", "L", ("R", "G", "B", "X")), - "RGBA": ("RGB", "L", ("R", "G", "B", "A")), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), - - # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and - # BGR;24. Use these modes only if you know exactly what you're - # doing... - -} - -try: - byteorder = sys.byteorder -except AttributeError: - import struct - if struct.unpack("h", "\0\1")[0] == 1: - byteorder = "big" - else: - byteorder = "little" - -if byteorder == 'little': - _ENDIAN = '<' -else: - _ENDIAN = '>' - -_MODE_CONV = { - # official modes - "1": ('|b1', None), # broken - "L": ('|u1', None), - "I": (_ENDIAN + 'i4', None), - "F": (_ENDIAN + 'f4', None), - "P": ('|u1', None), - "RGB": ('|u1', 3), - "RGBX": ('|u1', 4), - "RGBA": ('|u1', 4), - "CMYK": ('|u1', 4), - "YCbCr": ('|u1', 4), -} - -def _conv_type_shape(im): - shape = im.size[1], im.size[0] - typ, extra = _MODE_CONV[im.mode] - if extra is None: - return shape, typ - else: - return shape+(extra,), typ - - -MODES = _MODEINFO.keys() -MODES.sort() - -# raw modes that may be memory mapped. NOTE: if you change this, you -# may have to modify the stride calculation in map.c too! -_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") - -## -# Gets the "base" mode for given mode. This function returns "L" for -# images that contain grayscale data, and "RGB" for images that -# contain color data. -# -# @param mode Input mode. -# @return "L" or "RGB". -# @exception KeyError If the input mode was not a standard mode. - -def getmodebase(mode): - return ImageMode.getmode(mode).basemode - -## -# Gets the storage type mode. Given a mode, this function returns a -# single-layer mode suitable for storing individual bands. -# -# @param mode Input mode. -# @return "L", "I", or "F". -# @exception KeyError If the input mode was not a standard mode. - -def getmodetype(mode): - return ImageMode.getmode(mode).basetype - -## -# Gets a list of individual band names. Given a mode, this function -# returns a tuple containing the names of individual bands (use -# {@link #getmodetype} to get the mode used to store each individual -# band. -# -# @param mode Input mode. -# @return A tuple containing band names. The length of the tuple -# gives the number of bands in an image of the given mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebandnames(mode): - return ImageMode.getmode(mode).bands - -## -# Gets the number of individual bands for this mode. -# -# @param mode Input mode. -# @return The number of bands in this mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebands(mode): - return len(ImageMode.getmode(mode).bands) - -# -------------------------------------------------------------------- -# Helpers - -_initialized = 0 - -## -# Explicitly loads standard file format drivers. - -def preinit(): - "Load standard file format drivers." - - global _initialized - if _initialized >= 1: - return - - try: - import BmpImagePlugin - except ImportError: - pass - try: - import GifImagePlugin - except ImportError: - pass - try: - import JpegImagePlugin - except ImportError: - pass - try: - import PpmImagePlugin - except ImportError: - pass - try: - import PngImagePlugin - except ImportError: - pass -# try: -# import TiffImagePlugin -# except ImportError: -# pass - - _initialized = 1 - -## -# Explicitly initializes the Python Imaging Library. This function -# loads all available file format drivers. - -def init(): - "Load all file format drivers." - - global _initialized - if _initialized >= 2: - return 0 - - visited = {} - - directories = sys.path - - try: - directories = directories + [os.path.dirname(__file__)] - except NameError: - pass - - # only check directories (including current, if present in the path) - for directory in filter(isDirectory, directories): - fullpath = os.path.abspath(directory) - if visited.has_key(fullpath): - continue - for file in os.listdir(directory): - if file[-14:] == "ImagePlugin.py": - f, e = os.path.splitext(file) - try: - sys.path.insert(0, directory) - try: - __import__(f, globals(), locals(), []) - finally: - del sys.path[0] - except ImportError: - if DEBUG: - print "Image: failed to import", - print f, ":", sys.exc_value - visited[fullpath] = None - - if OPEN or SAVE: - _initialized = 2 - return 1 - -# -------------------------------------------------------------------- -# Codec factories (used by tostring/fromstring and ImageFile.load) - -def _getdecoder(mode, decoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get decoder - decoder = getattr(core, decoder_name + "_decoder") - # print decoder, (mode,) + args + extra - return apply(decoder, (mode,) + args + extra) - except AttributeError: - raise IOError("decoder %s not available" % decoder_name) - -def _getencoder(mode, encoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get encoder - encoder = getattr(core, encoder_name + "_encoder") - # print encoder, (mode,) + args + extra - return apply(encoder, (mode,) + args + extra) - except AttributeError: - raise IOError("encoder %s not available" % encoder_name) - - -# -------------------------------------------------------------------- -# Simple expression analyzer - -class _E: - def __init__(self, data): self.data = data - def __coerce__(self, other): return self, _E(other) - def __add__(self, other): return _E((self.data, "__add__", other.data)) - def __mul__(self, other): return _E((self.data, "__mul__", other.data)) - -def _getscaleoffset(expr): - stub = ["stub"] - data = expr(_E(stub)).data - try: - (a, b, c) = data # simplified syntax - if (a is stub and b == "__mul__" and isNumberType(c)): - return c, 0.0 - if (a is stub and b == "__add__" and isNumberType(c)): - return 1.0, c - except TypeError: pass - try: - ((a, b, c), d, e) = data # full syntax - if (a is stub and b == "__mul__" and isNumberType(c) and - d == "__add__" and isNumberType(e)): - return c, e - except TypeError: pass - raise ValueError("illegal expression") - - -# -------------------------------------------------------------------- -# Implementation wrapper - -## -# This class represents an image object. To create Image objects, use -# the appropriate factory functions. There's hardly ever any reason -# to call the Image constructor directly. -# -# @see #open -# @see #new -# @see #fromstring - -class Image: - - format = None - format_description = None - - def __init__(self): - # FIXME: take "new" parameters / other image? - # FIXME: turn mode and size into delegating properties? - self.im = None - self.mode = "" - self.size = (0, 0) - self.palette = None - self.info = {} - self.category = NORMAL - self.readonly = 0 - - def _new(self, im): - new = Image() - new.im = im - new.mode = im.mode - new.size = im.size - new.palette = self.palette - if im.mode == "P": - new.palette = ImagePalette.ImagePalette() - try: - new.info = self.info.copy() - except AttributeError: - # fallback (pre-1.5.2) - new.info = {} - for k, v in self.info: - new.info[k] = v - return new - - _makeself = _new # compatibility - - def _copy(self): - self.load() - self.im = self.im.copy() - self.readonly = 0 - - def _dump(self, file=None, format=None): - import tempfile - if not file: - file = tempfile.mktemp() - self.load() - if not format or format == "PPM": - self.im.save_ppm(file) - else: - file = file + "." + format - self.save(file, format) - return file - - def __repr__(self): - return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( - self.__class__.__module__, self.__class__.__name__, - self.mode, self.size[0], self.size[1], - id(self) - ) - - def __getattr__(self, name): - if name == "__array_interface__": - # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new['shape'] = shape - new['typestr'] = typestr - new['data'] = self.tostring() - return new - raise AttributeError(name) - - ## - # Returns a string containing pixel data. - # - # @param encoder_name What encoder to use. The default is to - # use the standard "raw" encoder. - # @param *args Extra arguments to the encoder. - # @return An 8-bit string. - - def tostring(self, encoder_name="raw", *args): - "Return image as a binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if encoder_name == "raw" and args == (): - args = self.mode - - self.load() - - # unpack data - e = _getencoder(self.mode, encoder_name, args) - e.setimage(self.im) - - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - - data = [] - while 1: - l, s, d = e.encode(bufsize) - data.append(d) - if s: - break - if s < 0: - raise RuntimeError("encoder error %d in tostring" % s) - - return string.join(data, "") - - ## - # Returns the image converted to an X11 bitmap. This method - # only works for mode "1" images. - # - # @param name The name prefix to use for the bitmap variables. - # @return A string containing an X11 bitmap. - # @exception ValueError If the mode is not "1" - - def tobitmap(self, name="image"): - "Return image as an XBM bitmap" - - self.load() - if self.mode != "1": - raise ValueError("not a bitmap") - data = self.tostring("xbm") - return string.join(["#define %s_width %d\n" % (name, self.size[0]), - "#define %s_height %d\n"% (name, self.size[1]), - "static char %s_bits[] = {\n" % name, data, "};"], "") - - ## - # Loads this image with pixel data from a string. - #

- # This method is similar to the {@link #fromstring} function, but - # loads data into this image instead of creating a new image - # object. - - def fromstring(self, data, decoder_name="raw", *args): - "Load data to image from binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - # default format - if decoder_name == "raw" and args == (): - args = self.mode - - # unpack data - d = _getdecoder(self.mode, decoder_name, args) - d.setimage(self.im) - s = d.decode(data) - - if s[0] >= 0: - raise ValueError("not enough image data") - if s[1] != 0: - raise ValueError("cannot decode image data") - - ## - # Allocates storage for the image and loads the pixel data. In - # normal cases, you don't need to call this method, since the - # Image class automatically loads an opened image when it is - # accessed for the first time. - # - # @return An image access object. - - def load(self): - "Explicitly load pixel data." - if self.im and self.palette and self.palette.dirty: - # realize palette - apply(self.im.putpalette, self.palette.getdata()) - self.palette.dirty = 0 - self.palette.mode = "RGB" - self.palette.rawmode = None - if self.info.has_key("transparency"): - self.im.putpalettealpha(self.info["transparency"], 0) - self.palette.mode = "RGBA" - if self.im: - return self.im.pixel_access(self.readonly) - - ## - # Verifies the contents of a file. For data read from a file, this - # method attempts to determine if the file is broken, without - # actually decoding the image data. If this method finds any - # problems, it raises suitable exceptions. If you need to load - # the image after using this method, you must reopen the image - # file. - - def verify(self): - "Verify file contents." - pass - - ## - # Returns a converted copy of this image. For the "P" mode, this - # method translates pixels through the palette. If mode is - # omitted, a mode is chosen so that all information in the image - # and the palette can be represented without a palette. - #

- # The current version supports all possible conversions between - # "L", "RGB" and "CMYK." - #

- # When translating a colour image to black and white (mode "L"), - # the library uses the ITU-R 601-2 luma transform: - #

- # L = R * 299/1000 + G * 587/1000 + B * 114/1000 - #

- # When translating a greyscale image into a bilevel image (mode - # "1"), all non-zero values are set to 255 (white). To use other - # thresholds, use the {@link #Image.point} method. - # - # @def convert(mode, matrix=None, **options) - # @param mode The requested mode. - # @param matrix An optional conversion matrix. If given, this - # should be 4- or 16-tuple containing floating point values. - # @param options Additional options, given as keyword arguments. - # @keyparam dither Dithering method, used when converting from - # mode "RGB" to "P". - # Available methods are NONE or FLOYDSTEINBERG (default). - # @keyparam palette Palette to use when converting from mode "RGB" - # to "P". Available palettes are WEB or ADAPTIVE. - # @keyparam colors Number of colors to use for the ADAPTIVE palette. - # Defaults to 256. - # @return An Image object. - - def convert(self, mode=None, data=None, dither=None, - palette=WEB, colors=256): - "Convert to other pixel format" - - if not mode: - # determine default mode - if self.mode == "P": - self.load() - if self.palette: - mode = self.palette.mode - else: - mode = "RGB" - else: - return self.copy() - - self.load() - - if data: - # matrix conversion - if mode not in ("L", "RGB"): - raise ValueError("illegal conversion") - im = self.im.convert_matrix(mode, data) - return self._new(im) - - if mode == "P" and palette == ADAPTIVE: - im = self.im.quantize(colors) - return self._new(im) - - # colourspace conversion - if dither is None: - dither = FLOYDSTEINBERG - - try: - im = self.im.convert(mode, dither) - except ValueError: - try: - # normalize source image and try again - im = self.im.convert(getmodebase(self.mode)) - im = im.convert(mode, dither) - except KeyError: - raise ValueError("illegal conversion") - - return self._new(im) - - def quantize(self, colors=256, method=0, kmeans=0, palette=None): - - # methods: - # 0 = median cut - # 1 = maximum coverage - - # NOTE: this functionality will be moved to the extended - # quantizer interface in a later version of PIL. - - self.load() - - if palette: - # use palette from reference image - palette.load() - if palette.mode != "P": - raise ValueError("bad mode for palette image") - if self.mode != "RGB" and self.mode != "L": - raise ValueError( - "only RGB or L mode images can be quantized to a palette" - ) - im = self.im.convert("P", 1, palette.im) - return self._makeself(im) - - im = self.im.quantize(colors, method, kmeans) - return self._new(im) - - ## - # Copies this image. Use this method if you wish to paste things - # into an image, but still retain the original. - # - # @return An Image object. - - def copy(self): - "Copy raster data" - - self.load() - im = self.im.copy() - return self._new(im) - - ## - # Returns a rectangular region from this image. The box is a - # 4-tuple defining the left, upper, right, and lower pixel - # coordinate. - #

- # This is a lazy operation. Changes to the source image may or - # may not be reflected in the cropped image. To break the - # connection, call the {@link #Image.load} method on the cropped - # copy. - # - # @param The crop rectangle, as a (left, upper, right, lower)-tuple. - # @return An Image object. - - def crop(self, box=None): - "Crop region from image" - - self.load() - if box is None: - return self.copy() - - # lazy operation - return _ImageCrop(self, box) - - ## - # Configures the image file loader so it returns a version of the - # image that as closely as possible matches the given mode and - # size. For example, you can use this method to convert a colour - # JPEG to greyscale while loading it, or to extract a 128x192 - # version from a PCD file. - #

- # Note that this method modifies the Image object in place. If - # the image has already been loaded, this method has no effect. - # - # @param mode The requested mode. - # @param size The requested size. - - def draft(self, mode, size): - "Configure image decoder" - - pass - - def _expand(self, xmargin, ymargin=None): - if ymargin is None: - ymargin = xmargin - self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) - - ## - # Filters this image using the given filter. For a list of - # available filters, see the ImageFilter module. - # - # @param filter Filter kernel. - # @return An Image object. - # @see ImageFilter - - def filter(self, filter): - "Apply environment filter to image" - - self.load() - - if callable(filter): - filter = filter() - if not hasattr(filter, "filter"): - raise TypeError("filter argument should be ImageFilter.Filter instance or class") - - if self.im.bands == 1: - return self._new(filter.filter(self.im)) - # fix to handle multiband images since _imaging doesn't - ims = [] - for c in range(self.im.bands): - ims.append(self._new(filter.filter(self.im.getband(c)))) - return merge(self.mode, ims) - - ## - # Returns a tuple containing the name of each band in this image. - # For example, getbands on an RGB image returns ("R", "G", "B"). - # - # @return A tuple containing band names. - - def getbands(self): - "Get band names" - - return ImageMode.getmode(self.mode).bands - - ## - # Calculates the bounding box of the non-zero regions in the - # image. - # - # @return The bounding box is returned as a 4-tuple defining the - # left, upper, right, and lower pixel coordinate. If the image - # is completely empty, this method returns None. - - def getbbox(self): - "Get bounding box of actual data (non-zero pixels) in image" - - self.load() - return self.im.getbbox() - - ## - # Returns a list of colors used in this image. - # - # @param maxcolors Maximum number of colors. If this number is - # exceeded, this method returns None. The default limit is - # 256 colors. - # @return An unsorted list of (count, pixel) values. - - def getcolors(self, maxcolors=256): - "Get colors from image, up to given limit" - - self.load() - if self.mode in ("1", "L", "P"): - h = self.im.histogram() - out = [] - for i in range(256): - if h[i]: - out.append((h[i], i)) - if len(out) > maxcolors: - return None - return out - return self.im.getcolors(maxcolors) - - ## - # Returns the contents of this image as a sequence object - # containing pixel values. The sequence object is flattened, so - # that values for line one follow directly after the values of - # line zero, and so on. - #

- # Note that the sequence object returned by this method is an - # internal PIL data type, which only supports certain sequence - # operations. To convert it to an ordinary sequence (e.g. for - # printing), use list(im.getdata()). - # - # @param band What band to return. The default is to return - # all bands. To return a single band, pass in the index - # value (e.g. 0 to get the "R" band from an "RGB" image). - # @return A sequence-like object. - - def getdata(self, band = None): - "Get image data as sequence object." - - self.load() - if band is not None: - return self.im.getband(band) - return self.im # could be abused - - ## - # Gets the the minimum and maximum pixel values for each band in - # the image. - # - # @return For a single-band image, a 2-tuple containing the - # minimum and maximum pixel value. For a multi-band image, - # a tuple containing one 2-tuple for each band. - - def getextrema(self): - "Get min/max value" - - self.load() - if self.im.bands > 1: - extrema = [] - for i in range(self.im.bands): - extrema.append(self.im.getband(i).getextrema()) - return tuple(extrema) - return self.im.getextrema() - - ## - # Returns a PyCObject that points to the internal image memory. - # - # @return A PyCObject object. - - def getim(self): - "Get PyCObject pointer to internal image memory" - - self.load() - return self.im.ptr - - - ## - # Returns the image palette as a list. - # - # @return A list of color values [r, g, b, ...], or None if the - # image has no palette. - - def getpalette(self): - "Get palette contents." - - self.load() - try: - return map(ord, self.im.getpalette()) - except ValueError: - return None # no palette - - - ## - # Returns the pixel value at a given position. - # - # @param xy The coordinate, given as (x, y). - # @return The pixel value. If the image is a multi-layer image, - # this method returns a tuple. - - def getpixel(self, xy): - "Get pixel value" - - self.load() - return self.im.getpixel(xy) - - ## - # Returns the horizontal and vertical projection. - # - # @return Two sequences, indicating where there are non-zero - # pixels along the X-axis and the Y-axis, respectively. - - def getprojection(self): - "Get projection to x and y axes" - - self.load() - x, y = self.im.getprojection() - return map(ord, x), map(ord, y) - - ## - # Returns a histogram for the image. The histogram is returned as - # a list of pixel counts, one for each pixel value in the source - # image. If the image has more than one band, the histograms for - # all bands are concatenated (for example, the histogram for an - # "RGB" image contains 768 values). - #

- # A bilevel image (mode "1") is treated as a greyscale ("L") image - # by this method. - #

- # If a mask is provided, the method returns a histogram for those - # parts of the image where the mask image is non-zero. The mask - # image must have the same size as the image, and be either a - # bi-level image (mode "1") or a greyscale image ("L"). - # - # @def histogram(mask=None) - # @param mask An optional mask. - # @return A list containing pixel counts. - - def histogram(self, mask=None, extrema=None): - "Take histogram of image" - - self.load() - if mask: - mask.load() - return self.im.histogram((0, 0), mask.im) - if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.histogram(extrema) - return self.im.histogram() - - ## - # (Deprecated) Returns a copy of the image where the data has been - # offset by the given distances. Data wraps around the edges. If - # yoffset is omitted, it is assumed to be equal to xoffset. - #

- # This method is deprecated. New code should use the offset - # function in the ImageChops module. - # - # @param xoffset The horizontal distance. - # @param yoffset The vertical distance. If omitted, both - # distances are set to the same value. - # @return An Image object. - - def offset(self, xoffset, yoffset=None): - "(deprecated) Offset image in horizontal and/or vertical direction" - if warnings: - warnings.warn( - "'offset' is deprecated; use 'ImageChops.offset' instead", - DeprecationWarning, stacklevel=2 - ) - import ImageChops - return ImageChops.offset(self, xoffset, yoffset) - - ## - # Pastes another image into this image. The box argument is either - # a 2-tuple giving the upper left corner, a 4-tuple defining the - # left, upper, right, and lower pixel coordinate, or None (same as - # (0, 0)). If a 4-tuple is given, the size of the pasted image - # must match the size of the region. - #

- # If the modes don't match, the pasted image is converted to the - # mode of this image (see the {@link #Image.convert} method for - # details). - #

- # Instead of an image, the source can be a integer or tuple - # containing pixel values. The method then fills the region - # with the given colour. When creating RGB images, you can - # also use colour strings as supported by the ImageColor module. - #

- # If a mask is given, this method updates only the regions - # indicated by the mask. You can use either "1", "L" or "RGBA" - # images (in the latter case, the alpha band is used as mask). - # Where the mask is 255, the given image is copied as is. Where - # the mask is 0, the current value is preserved. Intermediate - # values can be used for transparency effects. - #

- # Note that if you paste an "RGBA" image, the alpha band is - # ignored. You can work around this by using the same image as - # both source image and mask. - # - # @param im Source image or pixel value (integer or tuple). - # @param box An optional 4-tuple giving the region to paste into. - # If a 2-tuple is used instead, it's treated as the upper left - # corner. If omitted or None, the source is pasted into the - # upper left corner. - #

- # If an image is given as the second argument and there is no - # third, the box defaults to (0, 0), and the second argument - # is interpreted as a mask image. - # @param mask An optional mask image. - # @return An Image object. - - def paste(self, im, box=None, mask=None): - "Paste other image into region" - - if isImageType(box) and mask is None: - # abbreviated paste(im, mask) syntax - mask = box; box = None - - if box is None: - # cover all of self - box = (0, 0) + self.size - - if len(box) == 2: - # lower left corner given; get size from image or mask - if isImageType(im): - size = im.size - elif isImageType(mask): - size = mask.size - else: - # FIXME: use self.size here? - raise ValueError( - "cannot determine region size; use 4-item box" - ) - box = box + (box[0]+size[0], box[1]+size[1]) - - if isStringType(im): - import ImageColor - im = ImageColor.getcolor(im, self.mode) - - elif isImageType(im): - im.load() - if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): - # should use an adapter for this! - im = im.convert(self.mode) - im = im.im - - self.load() - if self.readonly: - self._copy() - - if mask: - mask.load() - self.im.paste(im, box, mask.im) - else: - self.im.paste(im, box) - - ## - # Maps this image through a lookup table or function. - # - # @param lut A lookup table, containing 256 values per band in the - # image. A function can be used instead, it should take a single - # argument. The function is called once for each possible pixel - # value, and the resulting table is applied to all bands of the - # image. - # @param mode Output mode (default is same as input). In the - # current version, this can only be used if the source image - # has mode "L" or "P", and the output has mode "1". - # @return An Image object. - - def point(self, lut, mode=None): - "Map image through lookup table" - - self.load() - - if isinstance(lut, ImagePointHandler): - return lut.point(self) - - if not isSequenceType(lut): - # if it isn't a list, it should be a function - if self.mode in ("I", "I;16", "F"): - # check if the function can be used with point_transform - scale, offset = _getscaleoffset(lut) - return self._new(self.im.point_transform(scale, offset)) - # for other modes, convert the function to a table - lut = map(lut, range(256)) * self.im.bands - - if self.mode == "F": - # FIXME: _imaging returns a confusing error message for this case - raise ValueError("point operation not supported for this mode") - - return self._new(self.im.point(lut, mode)) - - ## - # Adds or replaces the alpha layer in this image. If the image - # does not have an alpha layer, it's converted to "LA" or "RGBA". - # The new layer must be either "L" or "1". - # - # @param im The new alpha layer. This can either be an "L" or "1" - # image having the same size as this image, or an integer or - # other color value. - - def putalpha(self, alpha): - "Set alpha layer" - - self.load() - if self.readonly: - self._copy() - - if self.mode not in ("LA", "RGBA"): - # attempt to promote self to a matching alpha mode - try: - mode = getmodebase(self.mode) + "A" - try: - self.im.setmode(mode) - except (AttributeError, ValueError): - # do things the hard way - im = self.im.convert(mode) - if im.mode not in ("LA", "RGBA"): - raise ValueError # sanity check - self.im = im - self.mode = self.im.mode - except (KeyError, ValueError): - raise ValueError("illegal image mode") - - if self.mode == "LA": - band = 1 - else: - band = 3 - - if isImageType(alpha): - # alpha layer - if alpha.mode not in ("1", "L"): - raise ValueError("illegal image mode") - alpha.load() - if alpha.mode == "1": - alpha = alpha.convert("L") - else: - # constant alpha - try: - self.im.fillband(band, alpha) - except (AttributeError, ValueError): - # do things the hard way - alpha = new("L", self.size, alpha) - else: - return - - self.im.putband(alpha.im, band) - - ## - # Copies pixel data to this image. This method copies data from a - # sequence object into the image, starting at the upper left - # corner (0, 0), and continuing until either the image or the - # sequence ends. The scale and offset values are used to adjust - # the sequence values: pixel = value*scale + offset. - # - # @param data A sequence object. - # @param scale An optional scale value. The default is 1.0. - # @param offset An optional offset value. The default is 0.0. - - def putdata(self, data, scale=1.0, offset=0.0): - "Put data from a sequence object into an image." - - self.load() - if self.readonly: - self._copy() - - self.im.putdata(data, scale, offset) - - ## - # Attaches a palette to this image. The image must be a "P" or - # "L" image, and the palette sequence must contain 768 integer - # values, where each group of three values represent the red, - # green, and blue values for the corresponding pixel - # index. Instead of an integer sequence, you can use an 8-bit - # string. - # - # @def putpalette(data) - # @param data A palette sequence (either a list or a string). - - def putpalette(self, data, rawmode="RGB"): - "Put palette data into an image." - - if self.mode not in ("L", "P"): - raise ValueError("illegal image mode") - self.load() - if isinstance(data, ImagePalette.ImagePalette): - palette = ImagePalette.raw(data.rawmode, data.palette) - else: - if not isStringType(data): - data = string.join(map(chr, data), "") - palette = ImagePalette.raw(rawmode, data) - self.mode = "P" - self.palette = palette - self.palette.mode = "RGB" - self.load() # install new palette - - ## - # Modifies the pixel at the given position. The colour is given as - # a single numerical value for single-band images, and a tuple for - # multi-band images. - #

- # Note that this method is relatively slow. For more extensive - # changes, use {@link #Image.paste} or the ImageDraw module - # instead. - # - # @param xy The pixel coordinate, given as (x, y). - # @param value The pixel value. - # @see #Image.paste - # @see #Image.putdata - # @see ImageDraw - - def putpixel(self, xy, value): - "Set pixel value" - - self.load() - if self.readonly: - self._copy() - - return self.im.putpixel(xy, value) - - ## - # Returns a resized copy of this image. - # - # @def resize(size, filter=NEAREST) - # @param size The requested size in pixels, as a 2-tuple: - # (width, height). - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), BICUBIC - # (cubic spline interpolation in a 4x4 environment), or - # ANTIALIAS (a high-quality downsampling filter). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @return An Image object. - - def resize(self, size, resample=NEAREST): - "Resize image" - - if resample not in (NEAREST, BILINEAR, BICUBIC, ANTIALIAS): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - if resample == ANTIALIAS: - # requires stretch support (imToolkit & PIL 1.1.3) - try: - im = self.im.stretch(size, resample) - except AttributeError: - raise ValueError("unsupported resampling filter") - else: - im = self.im.resize(size, resample) - - return self._new(im) - - ## - # Returns a rotated copy of this image. This method returns a - # copy of this image, rotated the given number of degrees counter - # clockwise around its centre. - # - # @def rotate(angle, filter=NEAREST) - # @param angle In degrees counter clockwise. - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or BICUBIC - # (cubic spline interpolation in a 4x4 environment). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @param expand Optional expansion flag. If true, expands the output - # image to make it large enough to hold the entire rotated image. - # If false or omitted, make the output image the same size as the - # input image. - # @return An Image object. - - def rotate(self, angle, resample=NEAREST, expand=0): - "Rotate image. Angle given as degrees counter-clockwise." - - if expand: - import math - angle = -angle * math.pi / 180 - matrix = [ - math.cos(angle), math.sin(angle), 0.0, - -math.sin(angle), math.cos(angle), 0.0 - ] - def transform(x, y, (a, b, c, d, e, f)=matrix): - return a*x + b*y + c, d*x + e*y + f - - # calculate output size - w, h = self.size - xx = [] - yy = [] - for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y) - xx.append(x) - yy.append(y) - w = int(math.ceil(max(xx)) - math.floor(min(xx))) - h = int(math.ceil(max(yy)) - math.floor(min(yy))) - - # adjust center - x, y = transform(w / 2.0, h / 2.0) - matrix[2] = self.size[0] / 2.0 - x - matrix[5] = self.size[1] / 2.0 - y - - return self.transform((w, h), AFFINE, matrix, resample) - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - return self._new(self.im.rotate(angle, resample)) - - ## - # Saves this image under the given filename. If no format is - # specified, the format to use is determined from the filename - # extension, if possible. - #

- # Keyword options can be used to provide additional instructions - # to the writer. If a writer doesn't recognise an option, it is - # silently ignored. The available options are described later in - # this handbook. - #

- # You can use a file object instead of a filename. In this case, - # you must always specify the format. The file object must - # implement the seek, tell, and write - # methods, and be opened in binary mode. - # - # @def save(file, format=None, **options) - # @param file File name or file object. - # @param format Optional format override. If omitted, the - # format to use is determined from the filename extension. - # If a file object was used instead of a filename, this - # parameter should always be used. - # @param **options Extra parameters to the image writer. - # @return None - # @exception KeyError If the output format could not be determined - # from the file name. Use the format option to solve this. - # @exception IOError If the file could not be written. The file - # may have been created, and may contain partial data. - - def save(self, fp, format=None, **params): - "Save image to file or stream" - - if isStringType(fp): - filename = fp - else: - if hasattr(fp, "name") and isStringType(fp.name): - filename = fp.name - else: - filename = "" - - # may mutate self! - self.load() - - self.encoderinfo = params - self.encoderconfig = () - - preinit() - - ext = string.lower(os.path.splitext(filename)[1]) - - if not format: - try: - format = EXTENSION[ext] - except KeyError: - init() - try: - format = EXTENSION[ext] - except KeyError: - raise KeyError(ext) # unknown extension - - try: - save_handler = SAVE[string.upper(format)] - except KeyError: - init() - save_handler = SAVE[string.upper(format)] # unknown format - - if isStringType(fp): - import __builtin__ - fp = __builtin__.open(fp, "wb") - close = 1 - else: - close = 0 - - try: - save_handler(self, fp, filename) - finally: - # do what we can to clean up - if close: - fp.close() - - ## - # Seeks to the given frame in this sequence file. If you seek - # beyond the end of the sequence, the method raises an - # EOFError exception. When a sequence file is opened, the - # library automatically seeks to frame 0. - #

- # Note that in the current version of the library, most sequence - # formats only allows you to seek to the next frame. - # - # @param frame Frame number, starting at 0. - # @exception EOFError If the call attempts to seek beyond the end - # of the sequence. - # @see #Image.tell - - def seek(self, frame): - "Seek to given frame in sequence file" - - # overridden by file handlers - if frame != 0: - raise EOFError - - ## - # Displays this image. This method is mainly intended for - # debugging purposes. - #

- # On Unix platforms, this method saves the image to a temporary - # PPM file, and calls the xv utility. - #

- # On Windows, it saves the image to a temporary BMP file, and uses - # the standard BMP display utility to show it (usually Paint). - # - # @def show(title=None) - # @param title Optional title to use for the image window, - # where possible. - - def show(self, title=None, command=None): - "Display image (for debug purposes only)" - - _show(self, title=title, command=command) - - ## - # Split this image into individual bands. This method returns a - # tuple of individual image bands from an image. For example, - # splitting an "RGB" image creates three new images each - # containing a copy of one of the original bands (red, green, - # blue). - # - # @return A tuple containing bands. - - def split(self): - "Split image into bands" - - if self.im.bands == 1: - ims = [self.copy()] - else: - ims = [] - self.load() - for i in range(self.im.bands): - ims.append(self._new(self.im.getband(i))) - return tuple(ims) - - ## - # Returns the current frame number. - # - # @return Frame number, starting with 0. - # @see #Image.seek - - def tell(self): - "Return current frame number" - - return 0 - - ## - # Make this image into a thumbnail. This method modifies the - # image to contain a thumbnail version of itself, no larger than - # the given size. This method calculates an appropriate thumbnail - # size to preserve the aspect of the image, calls the {@link - # #Image.draft} method to configure the file reader (where - # applicable), and finally resizes the image. - #

- # Note that the bilinear and bicubic filters in the current - # version of PIL are not well-suited for thumbnail generation. - # You should use ANTIALIAS unless speed is much more - # important than quality. - #

- # Also note that this function modifies the Image object in place. - # If you need to use the full resolution image as well, apply this - # method to a {@link #Image.copy} of the original image. - # - # @param size Requested size. - # @param resample Optional resampling filter. This can be one - # of NEAREST, BILINEAR, BICUBIC, or - # ANTIALIAS (best quality). If omitted, it defaults - # to NEAREST (this will be changed to ANTIALIAS in a - # future version). - # @return None - - def thumbnail(self, size, resample=NEAREST): - "Create thumbnail representation (modifies image in place)" - - # FIXME: the default resampling filter will be changed - # to ANTIALIAS in future versions - - # preserve aspect ratio - x, y = self.size - if x > size[0]: y = max(y * size[0] / x, 1); x = size[0] - if y > size[1]: x = max(x * size[1] / y, 1); y = size[1] - size = x, y - - if size == self.size: - return - - self.draft(None, size) - - self.load() - - try: - im = self.resize(size, resample) - except ValueError: - if resample != ANTIALIAS: - raise - im = self.resize(size, NEAREST) # fallback - - self.im = im.im - self.mode = im.mode - self.size = size - - self.readonly = 0 - - # FIXME: the different tranform methods need further explanation - # instead of bloating the method docs, add a separate chapter. - - ## - # Transforms this image. This method creates a new image with the - # given size, and the same mode as the original, and copies data - # to the new image using the given transform. - #

- # @def transform(size, method, data, resample=NEAREST) - # @param size The output size. - # @param method The transformation method. This is one of - # EXTENT (cut out a rectangular subregion), AFFINE - # (affine transform), PERSPECTIVE (perspective - # transform), QUAD (map a quadrilateral to a - # rectangle), or MESH (map a number of source quadrilaterals - # in one operation). - # @param data Extra data to the transformation method. - # @param resample Optional resampling filter. It can be one of - # NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or - # BICUBIC (cubic spline interpolation in a 4x4 - # environment). If omitted, or if the image has mode - # "1" or "P", it is set to NEAREST. - # @return An Image object. - - def transform(self, size, method, data=None, resample=NEAREST, fill=1): - "Transform image" - - if isinstance(method, ImageTransformHandler): - return method.transform(size, self, resample=resample, fill=fill) - if hasattr(method, "getdata"): - # compatibility w. old-style transform objects - method, data = method.getdata() - if data is None: - raise ValueError("missing method data") - im = new(self.mode, size, None) - if method == MESH: - # list of quads - for box, quad in data: - im.__transformer(box, self, QUAD, quad, resample, fill) - else: - im.__transformer((0, 0)+size, self, method, data, resample, fill) - - return im - - def __transformer(self, box, image, method, data, - resample=NEAREST, fill=1): - - # FIXME: this should be turned into a lazy operation (?) - - w = box[2]-box[0] - h = box[3]-box[1] - - if method == AFFINE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4]) - elif method == EXTENT: - # convert extent to an affine transform - x0, y0, x1, y1 = data - xs = float(x1 - x0) / w - ys = float(y1 - y0) / h - method = AFFINE - data = (x0 + xs/2, xs, 0, y0 + ys/2, 0, ys) - elif method == PERSPECTIVE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4], - data[6], data[7]) - elif method == QUAD: - # quadrilateral warp. data specifies the four corners - # given as NW, SW, SE, and NE. - nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] - x0, y0 = nw; As = 1.0 / w; At = 1.0 / h - data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, - (se[0]-sw[0]-ne[0]+x0)*As*At, - y0, (ne[1]-y0)*As, (sw[1]-y0)*At, - (se[1]-sw[1]-ne[1]+y0)*As*At) - else: - raise ValueError("unknown transformation method") - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - image.load() - - self.load() - - if image.mode in ("1", "P"): - resample = NEAREST - - self.im.transform2(box, image.im, method, data, resample, fill) - - ## - # Returns a flipped or rotated copy of this image. - # - # @param method One of FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, - # ROTATE_90, ROTATE_180, or ROTATE_270. - - def transpose(self, method): - "Transpose image (flip or rotate in 90 degree steps)" - - self.load() - im = self.im.transpose(method) - return self._new(im) - -# -------------------------------------------------------------------- -# Lazy operations - -class _ImageCrop(Image): - - def __init__(self, im, box): - - Image.__init__(self) - - x0, y0, x1, y1 = box - if x1 < x0: - x1 = x0 - if y1 < y0: - y1 = y0 - - self.mode = im.mode - self.size = x1-x0, y1-y0 - - self.__crop = x0, y0, x1, y1 - - self.im = im.im - - def load(self): - - # lazy evaluation! - if self.__crop: - self.im = self.im.crop(self.__crop) - self.__crop = None - - if self.im: - return self.im.pixel_access(self.readonly) - - # FIXME: future versions should optimize crop/paste - # sequences! - -# -------------------------------------------------------------------- -# Abstract handlers. - -class ImagePointHandler: - # used as a mixin by point transforms (for use with im.point) - pass - -class ImageTransformHandler: - # used as a mixin by geometry transforms (for use with im.transform) - pass - -# -------------------------------------------------------------------- -# Factories - -# -# Debugging - -def _wedge(): - "Create greyscale wedge (for debugging only)" - - return Image()._new(core.wedge("L")) - -## -# Creates a new image with the given mode and size. -# -# @param mode The mode to use for the new image. -# @param size A 2-tuple, containing (width, height) in pixels. -# @param color What colour to use for the image. Default is black. -# If given, this should be a single integer or floating point value -# for single-band modes, and a tuple for multi-band modes (one value -# per band). When creating RGB images, you can also use colour -# strings as supported by the ImageColor module. If the colour is -# None, the image is not initialised. -# @return An Image object. - -def new(mode, size, color=0): - "Create a new image" - - if color is None: - # don't initialize - return Image()._new(core.new(mode, size)) - - if isStringType(color): - # css3-style specifier - - import ImageColor - color = ImageColor.getcolor(color, mode) - - return Image()._new(core.fill(mode, size, color)) - -## -# Creates an image memory from pixel data in a string. -#

-# In its simplest form, this function takes three arguments -# (mode, size, and unpacked pixel data). -#

-# You can also use any pixel decoder supported by PIL. For more -# information on available decoders, see the section Writing Your Own File Decoder. -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string containing raw data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. -# @return An Image object. - -def fromstring(mode, size, data, decoder_name="raw", *args): - "Load image from string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw" and args == (): - args = mode - - im = new(mode, size) - im.fromstring(data, decoder_name, args) - return im - -## -# (New in 1.1.4) Creates an image memory from pixel data in a string -# or byte buffer. -#

-# This function is similar to {@link #fromstring}, but uses data in -# the byte buffer, where possible. This means that changes to the -# original buffer object are reflected in this image). Not all modes -# can share memory; supported modes include "L", "RGBX", "RGBA", and -# "CMYK". -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image file in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -#

-# In the current version, the default parameters used for the "raw" -# decoder differs from that used for {@link fromstring}. This is a -# bug, and will probably be fixed in a future release. The current -# release issues a warning if you do this; to disable the warning, -# you should provide the full set of parameters. See below for -# details. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string or other buffer object containing raw -# data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. For the -# default encoder ("raw"), it's recommended that you provide the -# full set of parameters: -# frombuffer(mode, size, data, "raw", mode, 0, 1). -# @return An Image object. -# @since 1.1.4 - -def frombuffer(mode, size, data, decoder_name="raw", *args): - "Load image from string or buffer" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw": - if args == (): - if warnings: - warnings.warn( - "the frombuffer defaults may change in a future release; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, stacklevel=2 - ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 - if args[0] in _MAPMODES: - im = new(mode, (1,1)) - im = im._new( - core.map_buffer(data, size, decoder_name, None, 0, args) - ) - im.readonly = 1 - return im - - return fromstring(mode, size, data, decoder_name, args) - - -## -# (New in 1.1.6) Creates an image memory from an object exporting -# the array interface (using the buffer protocol). -# -# If obj is not contiguous, then the tostring method is called -# and {@link frombuffer} is used. -# -# @param obj Object with array interface -# @param mode Mode to use (will be determined from type if None) -# @return An image memory. - -def fromarray(obj, mode=None): - arr = obj.__array_interface__ - shape = arr['shape'] - ndim = len(shape) - try: - strides = arr['strides'] - except KeyError: - strides = None - if mode is None: - try: - typekey = (1, 1) + shape[2:], arr['typestr'] - mode, rawmode = _fromarray_typemap[typekey] - except KeyError: - # print typekey - raise TypeError("Cannot handle this data type") - else: - rawmode = mode - if mode in ["1", "L", "I", "P", "F"]: - ndmax = 2 - elif mode == "RGB": - ndmax = 3 - else: - ndmax = 4 - if ndim > ndmax: - raise ValueError("Too many dimensions.") - - size = shape[1], shape[0] - if strides is not None: - obj = obj.tostring() - - return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) - -_fromarray_typemap = { - # (shape, typestr) => mode, rawmode - # first two members of shape are set to one - # ((1, 1), "|b1"): ("1", "1"), # broken - ((1, 1), "|u1"): ("L", "L"), - ((1, 1), "|i1"): ("I", "I;8"), - ((1, 1), "i2"): ("I", "I;16B"), - ((1, 1), "i4"): ("I", "I;32B"), - ((1, 1), "f4"): ("F", "F;32BF"), - ((1, 1), "f8"): ("F", "F;64BF"), - ((1, 1, 3), "|u1"): ("RGB", "RGB"), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), - } - -# shortcuts -_fromarray_typemap[((1, 1), _ENDIAN + "i4")] = ("I", "I") -_fromarray_typemap[((1, 1), _ENDIAN + "f4")] = ("F", "F") - -## -# Opens and identifies the given image file. -#

-# This is a lazy operation; this function identifies the file, but the -# actual image data is not read from the file until you try to process -# the data (or call the {@link #Image.load} method). -# -# @def open(file, mode="r") -# @param file A filename (string) or a file object. The file object -# must implement read, seek, and tell methods, -# and be opened in binary mode. -# @param mode The mode. If given, this argument must be "r". -# @return An Image object. -# @exception IOError If the file cannot be found, or the image cannot be -# opened and identified. -# @see #new - -def open(fp, mode="r"): - "Open an image file, without loading the raster data" - - if mode != "r": - raise ValueError("bad mode") - - if isStringType(fp): - import __builtin__ - filename = fp - fp = __builtin__.open(fp, "rb") - else: - filename = "" - - prefix = fp.read(16) - - preinit() - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - if init(): - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - raise IOError("cannot identify image file") - -# -# Image processing. - -## -# Creates a new image by interpolating between two input images, using -# a constant alpha. -# -#

-#    out = image1 * (1.0 - alpha) + image2 * alpha
-# 
-# -# @param im1 The first image. -# @param im2 The second image. Must have the same mode and size as -# the first image. -# @param alpha The interpolation alpha factor. If alpha is 0.0, a -# copy of the first image is returned. If alpha is 1.0, a copy of -# the second image is returned. There are no restrictions on the -# alpha value. If necessary, the result is clipped to fit into -# the allowed output range. -# @return An Image object. - -def blend(im1, im2, alpha): - "Interpolate between images." - - im1.load() - im2.load() - return im1._new(core.blend(im1.im, im2.im, alpha)) - -## -# Creates a new image by interpolating between two input images, -# using the mask as alpha. -# -# @param image1 The first image. -# @param image2 The second image. Must have the same mode and -# size as the first image. -# @param mask A mask image. This image can can have mode -# "1", "L", or "RGBA", and must have the same size as the -# other two images. - -def composite(image1, image2, mask): - "Create composite image by blending images using a transparency mask" - - image = image2.copy() - image.paste(image1, None, mask) - return image - -## -# Applies the function (which should take one argument) to each pixel -# in the given image. If the image has more than one band, the same -# function is applied to each band. Note that the function is -# evaluated once for each possible pixel value, so you cannot use -# random components or other generators. -# -# @def eval(image, function) -# @param image The input image. -# @param function A function object, taking one integer argument. -# @return An Image object. - -def eval(image, *args): - "Evaluate image expression" - - return image.point(args[0]) - -## -# Creates a new image from a number of single-band images. -# -# @param mode The mode to use for the output image. -# @param bands A sequence containing one single-band image for -# each band in the output image. All bands must have the -# same size. -# @return An Image object. - -def merge(mode, bands): - "Merge a set of single band images into a new multiband image." - - if getmodebands(mode) != len(bands) or "*" in mode: - raise ValueError("wrong number of bands") - for im in bands[1:]: - if im.mode != getmodetype(mode): - raise ValueError("mode mismatch") - if im.size != bands[0].size: - raise ValueError("size mismatch") - im = core.new(mode, bands[0].size) - for i in range(getmodebands(mode)): - bands[i].load() - im.putband(bands[i].im, i) - return bands[0]._new(im) - -# -------------------------------------------------------------------- -# Plugin registry - -## -# Register an image file plugin. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param factory An image file factory method. -# @param accept An optional function that can be used to quickly -# reject images having another format. - -def register_open(id, factory, accept=None): - id = string.upper(id) - ID.append(id) - OPEN[id] = factory, accept - -## -# Registers an image MIME type. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param mimetype The image MIME type for this format. - -def register_mime(id, mimetype): - MIME[string.upper(id)] = mimetype - -## -# Registers an image save function. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param driver A function to save images in this format. - -def register_save(id, driver): - SAVE[string.upper(id)] = driver - -## -# Registers an image extension. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param extension An extension used for this format. - -def register_extension(id, extension): - EXTENSION[string.lower(extension)] = string.upper(id) - - -# -------------------------------------------------------------------- -# Simple display support. User code may override this. - -def _show(image, **options): - # override me, as necessary - apply(_showxv, (image,), options) - -def _showxv(image, title=None, **options): - import ImageShow - apply(ImageShow.show, (image, title), options) diff --git a/image_occlusion_enhanced/Imaging/PIL/ImageColor.py b/image_occlusion_enhanced/Imaging/PIL/ImageColor.py deleted file mode 100644 index c3cca46d..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/ImageColor.py +++ /dev/null @@ -1,263 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# map CSS3-style colour description strings to RGB -# -# History: -# 2002-10-24 fl Added support for CSS-style color strings -# 2002-12-15 fl Added RGBA support -# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2 -# 2004-07-19 fl Fixed gray/grey spelling issues -# 2009-03-05 fl Fixed rounding error in grayscale calculation -# -# Copyright (c) 2002-2004 by Secret Labs AB -# Copyright (c) 2002-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -import Image -import re, string - -try: - x = int("a", 16) -except TypeError: - # python 1.5.2 doesn't support int(x,b) - str2int = string.atoi -else: - str2int = int - -## -# Convert color string to RGB tuple. -# -# @param color A CSS3-style colour string. -# @return An RGB-tuple. -# @exception ValueError If the color string could not be interpreted -# as an RGB value. - -def getrgb(color): - # FIXME: add RGBA support - try: - rgb = colormap[color] - except KeyError: - try: - # fall back on case-insensitive lookup - rgb = colormap[string.lower(color)] - except KeyError: - rgb = None - # found color in cache - if rgb: - if isinstance(rgb, type(())): - return rgb - colormap[color] = rgb = getrgb(rgb) - return rgb - # check for known string formats - m = re.match("#\w\w\w$", color) - if m: - return ( - str2int(color[1]*2, 16), - str2int(color[2]*2, 16), - str2int(color[3]*2, 16) - ) - m = re.match("#\w\w\w\w\w\w$", color) - if m: - return ( - str2int(color[1:3], 16), - str2int(color[3:5], 16), - str2int(color[5:7], 16) - ) - m = re.match("rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) - if m: - return ( - str2int(m.group(1)), - str2int(m.group(2)), - str2int(m.group(3)) - ) - m = re.match("rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) - if m: - return ( - int((str2int(m.group(1)) * 255) / 100.0 + 0.5), - int((str2int(m.group(2)) * 255) / 100.0 + 0.5), - int((str2int(m.group(3)) * 255) / 100.0 + 0.5) - ) - m = re.match("hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) - if m: - from colorsys import hls_to_rgb - rgb = hls_to_rgb( - float(m.group(1)) / 360.0, - float(m.group(3)) / 100.0, - float(m.group(2)) / 100.0, - ) - return ( - int(rgb[0] * 255 + 0.5), - int(rgb[1] * 255 + 0.5), - int(rgb[2] * 255 + 0.5) - ) - raise ValueError("unknown color specifier: %r" % color) - -def getcolor(color, mode): - # same as getrgb, but converts the result to the given mode - color = getrgb(color) - if mode == "RGB": - return color - if mode == "RGBA": - r, g, b = color - return r, g, b, 255 - if Image.getmodebase(mode) == "L": - r, g, b = color - return (r*299 + g*587 + b*114)/1000 - return color - -colormap = { - # X11 colour table (from "CSS3 module: Color working draft"), with - # gray/grey spelling issues fixed. This is a superset of HTML 4.0 - # colour names used in CSS 1. - "aliceblue": "#f0f8ff", - "antiquewhite": "#faebd7", - "aqua": "#00ffff", - "aquamarine": "#7fffd4", - "azure": "#f0ffff", - "beige": "#f5f5dc", - "bisque": "#ffe4c4", - "black": "#000000", - "blanchedalmond": "#ffebcd", - "blue": "#0000ff", - "blueviolet": "#8a2be2", - "brown": "#a52a2a", - "burlywood": "#deb887", - "cadetblue": "#5f9ea0", - "chartreuse": "#7fff00", - "chocolate": "#d2691e", - "coral": "#ff7f50", - "cornflowerblue": "#6495ed", - "cornsilk": "#fff8dc", - "crimson": "#dc143c", - "cyan": "#00ffff", - "darkblue": "#00008b", - "darkcyan": "#008b8b", - "darkgoldenrod": "#b8860b", - "darkgray": "#a9a9a9", - "darkgrey": "#a9a9a9", - "darkgreen": "#006400", - "darkkhaki": "#bdb76b", - "darkmagenta": "#8b008b", - "darkolivegreen": "#556b2f", - "darkorange": "#ff8c00", - "darkorchid": "#9932cc", - "darkred": "#8b0000", - "darksalmon": "#e9967a", - "darkseagreen": "#8fbc8f", - "darkslateblue": "#483d8b", - "darkslategray": "#2f4f4f", - "darkslategrey": "#2f4f4f", - "darkturquoise": "#00ced1", - "darkviolet": "#9400d3", - "deeppink": "#ff1493", - "deepskyblue": "#00bfff", - "dimgray": "#696969", - "dimgrey": "#696969", - "dodgerblue": "#1e90ff", - "firebrick": "#b22222", - "floralwhite": "#fffaf0", - "forestgreen": "#228b22", - "fuchsia": "#ff00ff", - "gainsboro": "#dcdcdc", - "ghostwhite": "#f8f8ff", - "gold": "#ffd700", - "goldenrod": "#daa520", - "gray": "#808080", - "grey": "#808080", - "green": "#008000", - "greenyellow": "#adff2f", - "honeydew": "#f0fff0", - "hotpink": "#ff69b4", - "indianred": "#cd5c5c", - "indigo": "#4b0082", - "ivory": "#fffff0", - "khaki": "#f0e68c", - "lavender": "#e6e6fa", - "lavenderblush": "#fff0f5", - "lawngreen": "#7cfc00", - "lemonchiffon": "#fffacd", - "lightblue": "#add8e6", - "lightcoral": "#f08080", - "lightcyan": "#e0ffff", - "lightgoldenrodyellow": "#fafad2", - "lightgreen": "#90ee90", - "lightgray": "#d3d3d3", - "lightgrey": "#d3d3d3", - "lightpink": "#ffb6c1", - "lightsalmon": "#ffa07a", - "lightseagreen": "#20b2aa", - "lightskyblue": "#87cefa", - "lightslategray": "#778899", - "lightslategrey": "#778899", - "lightsteelblue": "#b0c4de", - "lightyellow": "#ffffe0", - "lime": "#00ff00", - "limegreen": "#32cd32", - "linen": "#faf0e6", - "magenta": "#ff00ff", - "maroon": "#800000", - "mediumaquamarine": "#66cdaa", - "mediumblue": "#0000cd", - "mediumorchid": "#ba55d3", - "mediumpurple": "#9370db", - "mediumseagreen": "#3cb371", - "mediumslateblue": "#7b68ee", - "mediumspringgreen": "#00fa9a", - "mediumturquoise": "#48d1cc", - "mediumvioletred": "#c71585", - "midnightblue": "#191970", - "mintcream": "#f5fffa", - "mistyrose": "#ffe4e1", - "moccasin": "#ffe4b5", - "navajowhite": "#ffdead", - "navy": "#000080", - "oldlace": "#fdf5e6", - "olive": "#808000", - "olivedrab": "#6b8e23", - "orange": "#ffa500", - "orangered": "#ff4500", - "orchid": "#da70d6", - "palegoldenrod": "#eee8aa", - "palegreen": "#98fb98", - "paleturquoise": "#afeeee", - "palevioletred": "#db7093", - "papayawhip": "#ffefd5", - "peachpuff": "#ffdab9", - "peru": "#cd853f", - "pink": "#ffc0cb", - "plum": "#dda0dd", - "powderblue": "#b0e0e6", - "purple": "#800080", - "red": "#ff0000", - "rosybrown": "#bc8f8f", - "royalblue": "#4169e1", - "saddlebrown": "#8b4513", - "salmon": "#fa8072", - "sandybrown": "#f4a460", - "seagreen": "#2e8b57", - "seashell": "#fff5ee", - "sienna": "#a0522d", - "silver": "#c0c0c0", - "skyblue": "#87ceeb", - "slateblue": "#6a5acd", - "slategray": "#708090", - "slategrey": "#708090", - "snow": "#fffafa", - "springgreen": "#00ff7f", - "steelblue": "#4682b4", - "tan": "#d2b48c", - "teal": "#008080", - "thistle": "#d8bfd8", - "tomato": "#ff6347", - "turquoise": "#40e0d0", - "violet": "#ee82ee", - "wheat": "#f5deb3", - "white": "#ffffff", - "whitesmoke": "#f5f5f5", - "yellow": "#ffff00", - "yellowgreen": "#9acd32", -} diff --git a/image_occlusion_enhanced/Imaging/PIL/ImageFile.py b/image_occlusion_enhanced/Imaging/PIL/ImageFile.py deleted file mode 100644 index 8a97c1b5..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/ImageFile.py +++ /dev/null @@ -1,528 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# base class for image file handlers -# -# history: -# 1995-09-09 fl Created -# 1996-03-11 fl Fixed load mechanism. -# 1996-04-15 fl Added pcx/xbm decoders. -# 1996-04-30 fl Added encoders. -# 1996-12-14 fl Added load helpers -# 1997-01-11 fl Use encode_to_file where possible -# 1997-08-27 fl Flush output in _save -# 1998-03-05 fl Use memory mapping for some modes -# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B" -# 1999-05-31 fl Added image parser -# 2000-10-12 fl Set readonly flag on memory-mapped images -# 2002-03-20 fl Use better messages for common decoder errors -# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available -# 2003-10-30 fl Added StubImageFile class -# 2004-02-25 fl Made incremental parser more robust -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1995-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -import Image -import traceback, string, os - -MAXBLOCK = 65536 - -SAFEBLOCK = 1024*1024 - -ERRORS = { - -1: "image buffer overrun error", - -2: "decoding error", - -3: "unknown error", - -8: "bad configuration", - -9: "out of memory error" -} - -def raise_ioerror(error): - try: - message = Image.core.getcodecstatus(error) - except AttributeError: - message = ERRORS.get(error) - if not message: - message = "decoder error %d" % error - raise IOError(message + " when reading image file") - -# -# -------------------------------------------------------------------- -# Helpers - -def _tilesort(t1, t2): - # sort on offset - return cmp(t1[2], t2[2]) - -# -# -------------------------------------------------------------------- -# ImageFile base class - -## -# Base class for image file handlers. - -class ImageFile(Image.Image): - "Base class for image file format handlers." - - def __init__(self, fp=None, filename=None): - Image.Image.__init__(self) - - self.tile = None - self.readonly = 1 # until we know better - - self.decoderconfig = () - self.decodermaxblock = MAXBLOCK - - if Image.isStringType(fp): - # filename - self.fp = open(fp, "rb") - self.filename = fp - else: - # stream - self.fp = fp - self.filename = filename - - try: - self._open() - except IndexError, v: # end of data - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError, v - except TypeError, v: # end of data (ord) - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError, v - except KeyError, v: # unsupported mode - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError, v - except EOFError, v: # got header but not the first frame - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError, v - - if not self.mode or self.size[0] <= 0: - raise SyntaxError, "not identified by this driver" - - def draft(self, mode, size): - "Set draft mode" - - pass - - def verify(self): - "Check file integrity" - - # raise exception if something's wrong. must be called - # directly after open, and closes file when finished. - self.fp = None - - def load(self): - "Load image data based on tile list" - - pixel = Image.Image.load(self) - - if self.tile is None: - raise IOError("cannot load this image") - if not self.tile: - return pixel - - self.map = None - - readonly = 0 - - if self.filename and len(self.tile) == 1: - # try memory mapping - d, e, o, a = self.tile[0] - if d == "raw" and a[0] == self.mode and a[0] in Image._MAPMODES: - try: - if hasattr(Image.core, "map"): - # use built-in mapper - self.map = Image.core.map(self.filename) - self.map.seek(o) - self.im = self.map.readimage( - self.mode, self.size, a[1], a[2] - ) - else: - # use mmap, if possible - import mmap - file = open(self.filename, "r+") - size = os.path.getsize(self.filename) - # FIXME: on Unix, use PROT_READ etc - self.map = mmap.mmap(file.fileno(), size) - self.im = Image.core.map_buffer( - self.map, self.size, d, e, o, a - ) - readonly = 1 - except (AttributeError, EnvironmentError, ImportError): - self.map = None - - self.load_prepare() - - # look for read/seek overrides - try: - read = self.load_read - except AttributeError: - read = self.fp.read - - try: - seek = self.load_seek - except AttributeError: - seek = self.fp.seek - - if not self.map: - - # sort tiles in file order - self.tile.sort(_tilesort) - - try: - # FIXME: This is a hack to handle TIFF's JpegTables tag. - prefix = self.tile_prefix - except AttributeError: - prefix = "" - - for d, e, o, a in self.tile: - d = Image._getdecoder(self.mode, d, a, self.decoderconfig) - seek(o) - try: - d.setimage(self.im, e) - except ValueError: - continue - b = prefix - t = len(b) - while 1: - s = read(self.decodermaxblock) - if not s: - self.tile = [] - raise IOError("image file is truncated (%d bytes not processed)" % len(b)) - b = b + s - n, e = d.decode(b) - if n < 0: - break - b = b[n:] - t = t + n - - self.tile = [] - self.readonly = readonly - - self.fp = None # might be shared - - if not self.map and e < 0: - raise_ioerror(e) - - # post processing - if hasattr(self, "tile_post_rotate"): - # FIXME: This is a hack to handle rotated PCD's - self.im = self.im.rotate(self.tile_post_rotate) - self.size = self.im.size - - self.load_end() - - return Image.Image.load(self) - - def load_prepare(self): - # create image memory if necessary - if not self.im or\ - self.im.mode != self.mode or self.im.size != self.size: - self.im = Image.core.new(self.mode, self.size) - # create palette (optional) - if self.mode == "P": - Image.Image.load(self) - - def load_end(self): - # may be overridden - pass - - # may be defined for contained formats - # def load_seek(self, pos): - # pass - - # may be defined for blocked formats (e.g. PNG) - # def load_read(self, bytes): - # pass - -## -# Base class for stub image loaders. -#

-# A stub loader is an image loader that can identify files of a -# certain format, but relies on external code to load the file. - -class StubImageFile(ImageFile): - "Base class for stub image loaders." - - def _open(self): - raise NotImplementedError( - "StubImageFile subclass must implement _open" - ) - - def load(self): - loader = self._load() - if loader is None: - raise IOError("cannot find loader for this %s file" % self.format) - image = loader.load(self) - assert image is not None - # become the other object (!) - self.__class__ = image.__class__ - self.__dict__ = image.__dict__ - - ## - # (Hook) Find actual image loader. - - def _load(self): - raise NotImplementedError( - "StubImageFile subclass must implement _load" - ) - -## -# (Internal) Support class for the Parser file. - -class _ParserFile: - # parser support class. - - def __init__(self, data): - self.data = data - self.offset = 0 - - def close(self): - self.data = self.offset = None - - def tell(self): - return self.offset - - def seek(self, offset, whence=0): - if whence == 0: - self.offset = offset - elif whence == 1: - self.offset = self.offset + offset - else: - # force error in Image.open - raise IOError("illegal argument to seek") - - def read(self, bytes=0): - pos = self.offset - if bytes: - data = self.data[pos:pos+bytes] - else: - data = self.data[pos:] - self.offset = pos + len(data) - return data - - def readline(self): - # FIXME: this is slow! - s = "" - while 1: - c = self.read(1) - if not c: - break - s = s + c - if c == "\n": - break - return s - -## -# Incremental image parser. This class implements the standard -# feed/close consumer interface. - -class Parser: - - incremental = None - image = None - data = None - decoder = None - finished = 0 - - ## - # (Consumer) Reset the parser. Note that you can only call this - # method immediately after you've created a parser; parser - # instances cannot be reused. - - def reset(self): - assert self.data is None, "cannot reuse parsers" - - ## - # (Consumer) Feed data to the parser. - # - # @param data A string buffer. - # @exception IOError If the parser failed to parse the image file. - - def feed(self, data): - # collect data - - if self.finished: - return - - if self.data is None: - self.data = data - else: - self.data = self.data + data - - # parse what we have - if self.decoder: - - if self.offset > 0: - # skip header - skip = min(len(self.data), self.offset) - self.data = self.data[skip:] - self.offset = self.offset - skip - if self.offset > 0 or not self.data: - return - - n, e = self.decoder.decode(self.data) - - if n < 0: - # end of stream - self.data = None - self.finished = 1 - if e < 0: - # decoding error - self.image = None - raise_ioerror(e) - else: - # end of image - return - self.data = self.data[n:] - - elif self.image: - - # if we end up here with no decoder, this file cannot - # be incrementally parsed. wait until we've gotten all - # available data - pass - - else: - - # attempt to open this file - try: - try: - fp = _ParserFile(self.data) - im = Image.open(fp) - finally: - fp.close() # explicitly close the virtual file - except IOError: - pass # not enough data - else: - flag = hasattr(im, "load_seek") or hasattr(im, "load_read") - if flag or len(im.tile) != 1: - # custom load code, or multiple tiles - self.decode = None - else: - # initialize decoder - im.load_prepare() - d, e, o, a = im.tile[0] - im.tile = [] - self.decoder = Image._getdecoder( - im.mode, d, a, im.decoderconfig - ) - self.decoder.setimage(im.im, e) - - # calculate decoder offset - self.offset = o - if self.offset <= len(self.data): - self.data = self.data[self.offset:] - self.offset = 0 - - self.image = im - - ## - # (Consumer) Close the stream. - # - # @return An image object. - # @exception IOError If the parser failed to parse the image file. - - def close(self): - # finish decoding - if self.decoder: - # get rid of what's left in the buffers - self.feed("") - self.data = self.decoder = None - if not self.finished: - raise IOError("image was incomplete") - if not self.image: - raise IOError("cannot parse this image") - if self.data: - # incremental parsing not possible; reopen the file - # not that we have all data - try: - fp = _ParserFile(self.data) - self.image = Image.open(fp) - finally: - self.image.load() - fp.close() # explicitly close the virtual file - return self.image - -# -------------------------------------------------------------------- - -## -# (Helper) Save image body to file. -# -# @param im Image object. -# @param fp File object. -# @param tile Tile list. - -def _save(im, fp, tile): - "Helper to save image based on tile list" - - im.load() - if not hasattr(im, "encoderconfig"): - im.encoderconfig = () - tile.sort(_tilesort) - # FIXME: make MAXBLOCK a configuration parameter - bufsize = max(MAXBLOCK, im.size[0] * 4) # see RawEncode.c - try: - fh = fp.fileno() - fp.flush() - except AttributeError: - # compress to Python file-compatible object - for e, b, o, a in tile: - e = Image._getencoder(im.mode, e, a, im.encoderconfig) - if o > 0: - fp.seek(o, 0) - e.setimage(im.im, b) - while 1: - l, s, d = e.encode(bufsize) - fp.write(d) - if s: - break - if s < 0: - raise IOError("encoder error %d when writing image file" % s) - else: - # slight speedup: compress to real file object - for e, b, o, a in tile: - e = Image._getencoder(im.mode, e, a, im.encoderconfig) - if o > 0: - fp.seek(o, 0) - e.setimage(im.im, b) - s = e.encode_to_file(fh, bufsize) - if s < 0: - raise IOError("encoder error %d when writing image file" % s) - try: - fp.flush() - except: pass - - -## -# Reads large blocks in a safe way. Unlike fp.read(n), this function -# doesn't trust the user. If the requested size is larger than -# SAFEBLOCK, the file is read block by block. -# -# @param fp File handle. Must implement a read method. -# @param size Number of bytes to read. -# @return A string containing up to size bytes of data. - -def _safe_read(fp, size): - if size <= 0: - return "" - if size <= SAFEBLOCK: - return fp.read(size) - data = [] - while size > 0: - block = fp.read(min(size, SAFEBLOCK)) - if not block: - break - data.append(block) - size = size - len(block) - return string.join(data, "") diff --git a/image_occlusion_enhanced/Imaging/PIL/ImageMode.py b/image_occlusion_enhanced/Imaging/PIL/ImageMode.py deleted file mode 100644 index 1d5df1c6..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/ImageMode.py +++ /dev/null @@ -1,50 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard mode descriptors -# -# History: -# 2006-03-20 fl Added -# -# Copyright (c) 2006 by Secret Labs AB. -# Copyright (c) 2006 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -# mode descriptor cache -_modes = {} - -## -# Wrapper for mode strings. - -class ModeDescriptor: - - def __init__(self, mode, bands, basemode, basetype): - self.mode = mode - self.bands = bands - self.basemode = basemode - self.basetype = basetype - - def __str__(self): - return self.mode - -## -# Gets a mode descriptor for the given mode. - -def getmode(mode): - if not _modes: - # initialize mode cache - import Image - # core modes - for m, (basemode, basetype, bands) in Image._MODEINFO.items(): - _modes[m] = ModeDescriptor(m, bands, basemode, basetype) - # extra experimental modes - _modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L") - _modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L") - # mapping modes - _modes["I;16"] = ModeDescriptor("I;16", "I", "L", "L") - _modes["I;16L"] = ModeDescriptor("I;16L", "I", "L", "L") - _modes["I;16B"] = ModeDescriptor("I;16B", "I", "L", "L") - return _modes[mode] diff --git a/image_occlusion_enhanced/Imaging/PIL/ImagePalette.py b/image_occlusion_enhanced/Imaging/PIL/ImagePalette.py deleted file mode 100644 index 6efee299..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/ImagePalette.py +++ /dev/null @@ -1,184 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# image palette object -# -# History: -# 1996-03-11 fl Rewritten. -# 1997-01-03 fl Up and running. -# 1997-08-23 fl Added load hack -# 2001-04-16 fl Fixed randint shadow bug in random() -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -import array -import Image, ImageColor - -## -# Colour palette wrapper for palette mapped images. - -class ImagePalette: - "Colour palette for palette mapped images" - - def __init__(self, mode = "RGB", palette = None): - self.mode = mode - self.rawmode = None # if set, palette contains raw data - self.palette = palette or range(256)*len(self.mode) - self.colors = {} - self.dirty = None - if len(self.mode)*256 != len(self.palette): - raise ValueError, "wrong palette size" - - def getdata(self): - # experimental: get palette contents in format suitable - # for the low-level im.putpalette primitive - if self.rawmode: - return self.rawmode, self.palette - return self.mode + ";L", self.tostring() - - def tostring(self): - # experimental: convert palette to string - if self.rawmode: - raise ValueError("palette contains raw palette data") - if Image.isStringType(self.palette): - return self.palette - return array.array("B", self.palette).tostring() - - def getcolor(self, color): - # experimental: given an rgb tuple, allocate palette entry - if self.rawmode: - raise ValueError("palette contains raw palette data") - if Image.isTupleType(color): - try: - return self.colors[color] - except KeyError: - # allocate new color slot - if Image.isStringType(self.palette): - self.palette = map(int, self.palette) - index = len(self.colors) - if index >= 256: - raise ValueError("cannot allocate more than 256 colors") - self.colors[color] = index - self.palette[index] = color[0] - self.palette[index+256] = color[1] - self.palette[index+512] = color[2] - self.dirty = 1 - return index - else: - raise ValueError("unknown color specifier: %r" % color) - - def save(self, fp): - # (experimental) save palette to text file - if self.rawmode: - raise ValueError("palette contains raw palette data") - if type(fp) == type(""): - fp = open(fp, "w") - fp.write("# Palette\n") - fp.write("# Mode: %s\n" % self.mode) - for i in range(256): - fp.write("%d" % i) - for j in range(i, len(self.palette), 256): - fp.write(" %d" % self.palette[j]) - fp.write("\n") - fp.close() - -# -------------------------------------------------------------------- -# Internal - -def raw(rawmode, data): - palette = ImagePalette() - palette.rawmode = rawmode - palette.palette = data - palette.dirty = 1 - return palette - -# -------------------------------------------------------------------- -# Factories - -def _make_linear_lut(black, white): - lut = [] - if black == 0: - for i in range(256): - lut.append(white*i/255) - else: - raise NotImplementedError # FIXME - return lut - -def _make_gamma_lut(exp, mode="RGB"): - lut = [] - for i in range(256): - lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5)) - return lut - -def new(mode, data): - return Image.core.new_palette(mode, data) - -def negative(mode="RGB"): - palette = range(256) - palette.reverse() - return ImagePalette(mode, palette * len(mode)) - -def random(mode="RGB"): - from random import randint - palette = [] - for i in range(256*len(mode)): - palette.append(randint(0, 255)) - return ImagePalette(mode, palette) - -def sepia(white="#fff0c0"): - r, g, b = ImageColor.getrgb(white) - r = _make_linear_lut(0, r) - g = _make_linear_lut(0, g) - b = _make_linear_lut(0, b) - return ImagePalette("RGB", r + g + b) - -def wedge(mode="RGB"): - return ImagePalette(mode, range(256) * len(mode)) - -def load(filename): - - # FIXME: supports GIMP gradients only - - fp = open(filename, "rb") - - lut = None - - if not lut: - try: - import GimpPaletteFile - fp.seek(0) - p = GimpPaletteFile.GimpPaletteFile(fp) - lut = p.getpalette() - except (SyntaxError, ValueError): - pass - - if not lut: - try: - import GimpGradientFile - fp.seek(0) - p = GimpGradientFile.GimpGradientFile(fp) - lut = p.getpalette() - except (SyntaxError, ValueError): - pass - - if not lut: - try: - import PaletteFile - fp.seek(0) - p = PaletteFile.PaletteFile(fp) - lut = p.getpalette() - except (SyntaxError, ValueError): - pass - - if not lut: - raise IOError, "cannot load palette" - - return lut # data, rawmode - - -# add some psuedocolour palettes as well diff --git a/image_occlusion_enhanced/Imaging/PIL/JpegImagePlugin.py b/image_occlusion_enhanced/Imaging/PIL/JpegImagePlugin.py deleted file mode 100644 index 933abf3c..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/JpegImagePlugin.py +++ /dev/null @@ -1,492 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# JPEG (JFIF) file handling -# -# See "Digital Compression and Coding of Continous-Tone Still Images, -# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1) -# -# History: -# 1995-09-09 fl Created -# 1995-09-13 fl Added full parser -# 1996-03-25 fl Added hack to use the IJG command line utilities -# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug -# 1996-05-28 fl Added draft support, JFIF version (0.1) -# 1996-12-30 fl Added encoder options, added progression property (0.2) -# 1997-08-27 fl Save mode 1 images as BW (0.3) -# 1998-07-12 fl Added YCbCr to draft and save methods (0.4) -# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1) -# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2) -# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3) -# 2003-04-25 fl Added experimental EXIF decoder (0.5) -# 2003-06-06 fl Added experimental EXIF GPSinfo decoder -# 2003-09-13 fl Extract COM markers -# 2009-09-06 fl Added icc_profile support (from Florian Hoech) -# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6) -# 2009-03-08 fl Added subsampling support (from Justin Huff). -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -__version__ = "0.6" - -import array, struct -import string -import Image, ImageFile - -def i16(c,o=0): - return ord(c[o+1]) + (ord(c[o])<<8) - -def i32(c,o=0): - return ord(c[o+3]) + (ord(c[o+2])<<8) + (ord(c[o+1])<<16) + (ord(c[o])<<24) - -# -# Parser - -def Skip(self, marker): - n = i16(self.fp.read(2))-2 - ImageFile._safe_read(self.fp, n) - -def APP(self, marker): - # - # Application marker. Store these in the APP dictionary. - # Also look for well-known application markers. - - n = i16(self.fp.read(2))-2 - s = ImageFile._safe_read(self.fp, n) - - app = "APP%d" % (marker&15) - - self.app[app] = s # compatibility - self.applist.append((app, s)) - - if marker == 0xFFE0 and s[:4] == "JFIF": - # extract JFIF information - self.info["jfif"] = version = i16(s, 5) # version - self.info["jfif_version"] = divmod(version, 256) - # extract JFIF properties - try: - jfif_unit = ord(s[7]) - jfif_density = i16(s, 8), i16(s, 10) - except: - pass - else: - if jfif_unit == 1: - self.info["dpi"] = jfif_density - self.info["jfif_unit"] = jfif_unit - self.info["jfif_density"] = jfif_density - elif marker == 0xFFE1 and s[:5] == "Exif\0": - # extract Exif information (incomplete) - self.info["exif"] = s # FIXME: value will change - elif marker == 0xFFE2 and s[:5] == "FPXR\0": - # extract FlashPix information (incomplete) - self.info["flashpix"] = s # FIXME: value will change - elif marker == 0xFFE2 and s[:12] == "ICC_PROFILE\0": - # Since an ICC profile can be larger than the maximum size of - # a JPEG marker (64K), we need provisions to split it into - # multiple markers. The format defined by the ICC specifies - # one or more APP2 markers containing the following data: - # Identifying string ASCII "ICC_PROFILE\0" (12 bytes) - # Marker sequence number 1, 2, etc (1 byte) - # Number of markers Total of APP2's used (1 byte) - # Profile data (remainder of APP2 data) - # Decoders should use the marker sequence numbers to - # reassemble the profile, rather than assuming that the APP2 - # markers appear in the correct sequence. - self.icclist.append(s) - elif marker == 0xFFEE and s[:5] == "Adobe": - self.info["adobe"] = i16(s, 5) - # extract Adobe custom properties - try: - adobe_transform = ord(s[1]) - except: - pass - else: - self.info["adobe_transform"] = adobe_transform - -def COM(self, marker): - # - # Comment marker. Store these in the APP dictionary. - - n = i16(self.fp.read(2))-2 - s = ImageFile._safe_read(self.fp, n) - - self.app["COM"] = s # compatibility - self.applist.append(("COM", s)) - -def SOF(self, marker): - # - # Start of frame marker. Defines the size and mode of the - # image. JPEG is colour blind, so we use some simple - # heuristics to map the number of layers to an appropriate - # mode. Note that this could be made a bit brighter, by - # looking for JFIF and Adobe APP markers. - - n = i16(self.fp.read(2))-2 - s = ImageFile._safe_read(self.fp, n) - self.size = i16(s[3:]), i16(s[1:]) - - self.bits = ord(s[0]) - if self.bits != 8: - raise SyntaxError("cannot handle %d-bit layers" % self.bits) - - self.layers = ord(s[5]) - if self.layers == 1: - self.mode = "L" - elif self.layers == 3: - self.mode = "RGB" - elif self.layers == 4: - self.mode = "CMYK" - else: - raise SyntaxError("cannot handle %d-layer images" % self.layers) - - if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: - self.info["progressive"] = self.info["progression"] = 1 - - if self.icclist: - # fixup icc profile - self.icclist.sort() # sort by sequence number - if ord(self.icclist[0][13]) == len(self.icclist): - profile = [] - for p in self.icclist: - profile.append(p[14:]) - icc_profile = string.join(profile, "") - else: - icc_profile = None # wrong number of fragments - self.info["icc_profile"] = icc_profile - self.icclist = None - - for i in range(6, len(s), 3): - t = s[i:i+3] - # 4-tuples: id, vsamp, hsamp, qtable - self.layer.append((t[0], ord(t[1])/16, ord(t[1])&15, ord(t[2]))) - -def DQT(self, marker): - # - # Define quantization table. Support baseline 8-bit tables - # only. Note that there might be more than one table in - # each marker. - - # FIXME: The quantization tables can be used to estimate the - # compression quality. - - n = i16(self.fp.read(2))-2 - s = ImageFile._safe_read(self.fp, n) - while len(s): - if len(s) < 65: - raise SyntaxError("bad quantization table marker") - v = ord(s[0]) - if v/16 == 0: - self.quantization[v&15] = array.array("b", s[1:65]) - s = s[65:] - else: - return # FIXME: add code to read 16-bit tables! - # raise SyntaxError, "bad quantization table element size" - - -# -# JPEG marker table - -MARKER = { - 0xFFC0: ("SOF0", "Baseline DCT", SOF), - 0xFFC1: ("SOF1", "Extended Sequential DCT", SOF), - 0xFFC2: ("SOF2", "Progressive DCT", SOF), - 0xFFC3: ("SOF3", "Spatial lossless", SOF), - 0xFFC4: ("DHT", "Define Huffman table", Skip), - 0xFFC5: ("SOF5", "Differential sequential DCT", SOF), - 0xFFC6: ("SOF6", "Differential progressive DCT", SOF), - 0xFFC7: ("SOF7", "Differential spatial", SOF), - 0xFFC8: ("JPG", "Extension", None), - 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF), - 0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF), - 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF), - 0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip), - 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF), - 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF), - 0xFFCF: ("SOF15", "Differential spatial (AC)", SOF), - 0xFFD0: ("RST0", "Restart 0", None), - 0xFFD1: ("RST1", "Restart 1", None), - 0xFFD2: ("RST2", "Restart 2", None), - 0xFFD3: ("RST3", "Restart 3", None), - 0xFFD4: ("RST4", "Restart 4", None), - 0xFFD5: ("RST5", "Restart 5", None), - 0xFFD6: ("RST6", "Restart 6", None), - 0xFFD7: ("RST7", "Restart 7", None), - 0xFFD8: ("SOI", "Start of image", None), - 0xFFD9: ("EOI", "End of image", None), - 0xFFDA: ("SOS", "Start of scan", Skip), - 0xFFDB: ("DQT", "Define quantization table", DQT), - 0xFFDC: ("DNL", "Define number of lines", Skip), - 0xFFDD: ("DRI", "Define restart interval", Skip), - 0xFFDE: ("DHP", "Define hierarchical progression", SOF), - 0xFFDF: ("EXP", "Expand reference component", Skip), - 0xFFE0: ("APP0", "Application segment 0", APP), - 0xFFE1: ("APP1", "Application segment 1", APP), - 0xFFE2: ("APP2", "Application segment 2", APP), - 0xFFE3: ("APP3", "Application segment 3", APP), - 0xFFE4: ("APP4", "Application segment 4", APP), - 0xFFE5: ("APP5", "Application segment 5", APP), - 0xFFE6: ("APP6", "Application segment 6", APP), - 0xFFE7: ("APP7", "Application segment 7", APP), - 0xFFE8: ("APP8", "Application segment 8", APP), - 0xFFE9: ("APP9", "Application segment 9", APP), - 0xFFEA: ("APP10", "Application segment 10", APP), - 0xFFEB: ("APP11", "Application segment 11", APP), - 0xFFEC: ("APP12", "Application segment 12", APP), - 0xFFED: ("APP13", "Application segment 13", APP), - 0xFFEE: ("APP14", "Application segment 14", APP), - 0xFFEF: ("APP15", "Application segment 15", APP), - 0xFFF0: ("JPG0", "Extension 0", None), - 0xFFF1: ("JPG1", "Extension 1", None), - 0xFFF2: ("JPG2", "Extension 2", None), - 0xFFF3: ("JPG3", "Extension 3", None), - 0xFFF4: ("JPG4", "Extension 4", None), - 0xFFF5: ("JPG5", "Extension 5", None), - 0xFFF6: ("JPG6", "Extension 6", None), - 0xFFF7: ("JPG7", "Extension 7", None), - 0xFFF8: ("JPG8", "Extension 8", None), - 0xFFF9: ("JPG9", "Extension 9", None), - 0xFFFA: ("JPG10", "Extension 10", None), - 0xFFFB: ("JPG11", "Extension 11", None), - 0xFFFC: ("JPG12", "Extension 12", None), - 0xFFFD: ("JPG13", "Extension 13", None), - 0xFFFE: ("COM", "Comment", COM) -} - - -def _accept(prefix): - return prefix[0] == "\377" - -## -# Image plugin for JPEG and JFIF images. - -class JpegImageFile(ImageFile.ImageFile): - - format = "JPEG" - format_description = "JPEG (ISO 10918)" - - def _open(self): - - s = self.fp.read(1) - - if ord(s[0]) != 255: - raise SyntaxError("not a JPEG file") - - # Create attributes - self.bits = self.layers = 0 - - # JPEG specifics (internal) - self.layer = [] - self.huffman_dc = {} - self.huffman_ac = {} - self.quantization = {} - self.app = {} # compatibility - self.applist = [] - self.icclist = [] - - while 1: - - s = s + self.fp.read(1) - - i = i16(s) - - if MARKER.has_key(i): - name, description, handler = MARKER[i] - # print hex(i), name, description - if handler is not None: - handler(self, i) - if i == 0xFFDA: # start of scan - rawmode = self.mode - if self.mode == "CMYK": - rawmode = "CMYK;I" # assume adobe conventions - self.tile = [("jpeg", (0,0) + self.size, 0, (rawmode, ""))] - # self.__offset = self.fp.tell() - break - s = self.fp.read(1) - elif i == 0 or i == 65535: - # padded marker or junk; move on - s = "\xff" - else: - raise SyntaxError("no marker found") - - def draft(self, mode, size): - - if len(self.tile) != 1: - return - - d, e, o, a = self.tile[0] - scale = 0 - - if a[0] == "RGB" and mode in ["L", "YCbCr"]: - self.mode = mode - a = mode, "" - - if size: - scale = max(self.size[0] / size[0], self.size[1] / size[1]) - for s in [8, 4, 2, 1]: - if scale >= s: - break - e = e[0], e[1], (e[2]-e[0]+s-1)/s+e[0], (e[3]-e[1]+s-1)/s+e[1] - self.size = ((self.size[0]+s-1)/s, (self.size[1]+s-1)/s) - scale = s - - self.tile = [(d, e, o, a)] - self.decoderconfig = (scale, 1) - - return self - - def load_djpeg(self): - - # ALTERNATIVE: handle JPEGs via the IJG command line utilities - - import tempfile, os - file = tempfile.mktemp() - os.system("djpeg %s >%s" % (self.filename, file)) - - try: - self.im = Image.core.open_ppm(file) - finally: - try: os.unlink(file) - except: pass - - self.mode = self.im.mode - self.size = self.im.size - - self.tile = [] - - def _getexif(self): - # Extract EXIF information. This method is highly experimental, - # and is likely to be replaced with something better in a future - # version. - import TiffImagePlugin, StringIO - def fixup(value): - if len(value) == 1: - return value[0] - return value - # The EXIF record consists of a TIFF file embedded in a JPEG - # application marker (!). - try: - data = self.info["exif"] - except KeyError: - return None - file = StringIO.StringIO(data[6:]) - head = file.read(8) - exif = {} - # process dictionary - info = TiffImagePlugin.ImageFileDirectory(head) - info.load(file) - for key, value in info.items(): - exif[key] = fixup(value) - # get exif extension - try: - file.seek(exif[0x8769]) - except KeyError: - pass - else: - info = TiffImagePlugin.ImageFileDirectory(head) - info.load(file) - for key, value in info.items(): - exif[key] = fixup(value) - # get gpsinfo extension - try: - file.seek(exif[0x8825]) - except KeyError: - pass - else: - info = TiffImagePlugin.ImageFileDirectory(head) - info.load(file) - exif[0x8825] = gps = {} - for key, value in info.items(): - gps[key] = fixup(value) - return exif - -# -------------------------------------------------------------------- -# stuff to save JPEG files - -RAWMODE = { - "1": "L", - "L": "L", - "RGB": "RGB", - "RGBA": "RGB", - "RGBX": "RGB", - "CMYK": "CMYK;I", # assume adobe conventions - "YCbCr": "YCbCr", -} - -def _save(im, fp, filename): - - try: - rawmode = RAWMODE[im.mode] - except KeyError: - raise IOError("cannot write mode %s as JPEG" % im.mode) - - info = im.encoderinfo - - dpi = info.get("dpi", (0, 0)) - - subsampling = info.get("subsampling", -1) - if subsampling == "4:4:4": - subsampling = 0 - elif subsampling == "4:2:2": - subsampling = 1 - elif subsampling == "4:1:1": - subsampling = 2 - - extra = "" - - icc_profile = info.get("icc_profile") - if icc_profile: - ICC_OVERHEAD_LEN = 14 - MAX_BYTES_IN_MARKER = 65533 - MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN - markers = [] - while icc_profile: - markers.append(icc_profile[:MAX_DATA_BYTES_IN_MARKER]) - icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] - i = 1 - for marker in markers: - size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker)) - extra = extra + ("\xFF\xE2" + size + "ICC_PROFILE\0" + chr(i) + chr(len(markers)) + marker) - i = i + 1 - - # get keyword arguments - im.encoderconfig = ( - info.get("quality", 0), - # "progressive" is the official name, but older documentation - # says "progression" - # FIXME: issue a warning if the wrong form is used (post-1.1.7) - info.has_key("progressive") or info.has_key("progression"), - info.get("smooth", 0), - info.has_key("optimize"), - info.get("streamtype", 0), - dpi[0], dpi[1], - subsampling, - extra, - ) - - ImageFile._save(im, fp, [("jpeg", (0,0)+im.size, 0, rawmode)]) - -def _save_cjpeg(im, fp, filename): - # ALTERNATIVE: handle JPEGs via the IJG command line utilities. - import os - file = im._dump() - os.system("cjpeg %s >%s" % (file, filename)) - try: os.unlink(file) - except: pass - -# -------------------------------------------------------------------q- -# Registry stuff - -Image.register_open("JPEG", JpegImageFile, _accept) -Image.register_save("JPEG", _save) - -Image.register_extension("JPEG", ".jfif") -Image.register_extension("JPEG", ".jpe") -Image.register_extension("JPEG", ".jpg") -Image.register_extension("JPEG", ".jpeg") - -Image.register_mime("JPEG", "image/jpeg") diff --git a/image_occlusion_enhanced/Imaging/PIL/PngImagePlugin.py b/image_occlusion_enhanced/Imaging/PIL/PngImagePlugin.py deleted file mode 100644 index 0ee8589a..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/PngImagePlugin.py +++ /dev/null @@ -1,620 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PNG support code -# -# See "PNG (Portable Network Graphics) Specification, version 1.0; -# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.). -# -# history: -# 1996-05-06 fl Created (couldn't resist it) -# 1996-12-14 fl Upgraded, added read and verify support (0.2) -# 1996-12-15 fl Separate PNG stream parser -# 1996-12-29 fl Added write support, added getchunks -# 1996-12-30 fl Eliminated circular references in decoder (0.3) -# 1998-07-12 fl Read/write 16-bit images as mode I (0.4) -# 2001-02-08 fl Added transparency support (from Zircon) (0.5) -# 2001-04-16 fl Don't close data source in "open" method (0.6) -# 2004-02-24 fl Don't even pretend to support interlaced files (0.7) -# 2004-08-31 fl Do basic sanity check on chunk identifiers (0.8) -# 2004-09-20 fl Added PngInfo chunk container -# 2004-12-18 fl Added DPI read support (based on code by Niki Spahiev) -# 2008-08-13 fl Added tRNS support for RGB images -# 2009-03-06 fl Support for preserving ICC profiles (by Florian Hoech) -# 2009-03-08 fl Added zTXT support (from Lowell Alleman) -# 2009-03-29 fl Read interlaced PNG files (from Conrado Porto Lopes Gouvua) -# -# Copyright (c) 1997-2009 by Secret Labs AB -# Copyright (c) 1996 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -__version__ = "0.9" - -import re, string - -import Image, ImageFile, ImagePalette, zlib - - -def i16(c): - return ord(c[1]) + (ord(c[0])<<8) -def i32(c): - return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24) - -is_cid = re.compile("\w\w\w\w").match - - -_MAGIC = "\211PNG\r\n\032\n" - - -_MODES = { - # supported bits/color combinations, and corresponding modes/rawmodes - (1, 0): ("1", "1"), - (2, 0): ("L", "L;2"), - (4, 0): ("L", "L;4"), - (8, 0): ("L", "L"), - (16,0): ("I", "I;16B"), - (8, 2): ("RGB", "RGB"), - (16,2): ("RGB", "RGB;16B"), - (1, 3): ("P", "P;1"), - (2, 3): ("P", "P;2"), - (4, 3): ("P", "P;4"), - (8, 3): ("P", "P"), - (8, 4): ("LA", "LA"), - (16,4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available - (8, 6): ("RGBA", "RGBA"), - (16,6): ("RGBA", "RGBA;16B"), -} - - -# -------------------------------------------------------------------- -# Support classes. Suitable for PNG and related formats like MNG etc. - -class ChunkStream: - - def __init__(self, fp): - - self.fp = fp - self.queue = [] - - if not hasattr(Image.core, "crc32"): - self.crc = self.crc_skip - - def read(self): - "Fetch a new chunk. Returns header information." - - if self.queue: - cid, pos, len = self.queue[-1] - del self.queue[-1] - self.fp.seek(pos) - else: - s = self.fp.read(8) - cid = s[4:] - pos = self.fp.tell() - len = i32(s) - - if not is_cid(cid): - raise SyntaxError, "broken PNG file (chunk %s)" % repr(cid) - - return cid, pos, len - - def close(self): - self.queue = self.crc = self.fp = None - - def push(self, cid, pos, len): - - self.queue.append((cid, pos, len)) - - def call(self, cid, pos, len): - "Call the appropriate chunk handler" - - if Image.DEBUG: - print "STREAM", cid, pos, len - return getattr(self, "chunk_" + cid)(pos, len) - - def crc(self, cid, data): - "Read and verify checksum" - - crc1 = Image.core.crc32(data, Image.core.crc32(cid)) - crc2 = i16(self.fp.read(2)), i16(self.fp.read(2)) - if crc1 != crc2: - raise SyntaxError, "broken PNG file"\ - "(bad header checksum in %s)" % cid - - def crc_skip(self, cid, data): - "Read checksum. Used if the C module is not present" - - self.fp.read(4) - - def verify(self, endchunk = "IEND"): - - # Simple approach; just calculate checksum for all remaining - # blocks. Must be called directly after open. - - cids = [] - - while 1: - cid, pos, len = self.read() - if cid == endchunk: - break - self.crc(cid, ImageFile._safe_read(self.fp, len)) - cids.append(cid) - - return cids - - -# -------------------------------------------------------------------- -# PNG chunk container (for use with save(pnginfo=)) - -class PngInfo: - - def __init__(self): - self.chunks = [] - - def add(self, cid, data): - self.chunks.append((cid, data)) - - def add_text(self, key, value, zip=0): - if zip: - import zlib - self.add("zTXt", key + "\0\0" + zlib.compress(value)) - else: - self.add("tEXt", key + "\0" + value) - -# -------------------------------------------------------------------- -# PNG image stream (IHDR/IEND) - -class PngStream(ChunkStream): - - def __init__(self, fp): - - ChunkStream.__init__(self, fp) - - # local copies of Image attributes - self.im_info = {} - self.im_text = {} - self.im_size = (0,0) - self.im_mode = None - self.im_tile = None - self.im_palette = None - - def chunk_iCCP(self, pos, len): - - # ICC profile - s = ImageFile._safe_read(self.fp, len) - # according to PNG spec, the iCCP chunk contains: - # Profile name 1-79 bytes (character string) - # Null separator 1 byte (null character) - # Compression method 1 byte (0) - # Compressed profile n bytes (zlib with deflate compression) - i = string.find(s, chr(0)) - if Image.DEBUG: - print "iCCP profile name", s[:i] - print "Compression method", ord(s[i]) - comp_method = ord(s[i]) - if comp_method != 0: - raise SyntaxError("Unknown compression method %s in iCCP chunk" % comp_method) - try: - icc_profile = zlib.decompress(s[i+2:]) - except zlib.error: - icc_profile = None # FIXME - self.im_info["icc_profile"] = icc_profile - return s - - def chunk_IHDR(self, pos, len): - - # image header - s = ImageFile._safe_read(self.fp, len) - self.im_size = i32(s), i32(s[4:]) - try: - self.im_mode, self.im_rawmode = _MODES[(ord(s[8]), ord(s[9]))] - except: - pass - if ord(s[12]): - self.im_info["interlace"] = 1 - if ord(s[11]): - raise SyntaxError, "unknown filter category" - return s - - def chunk_IDAT(self, pos, len): - - # image data - self.im_tile = [("zip", (0,0)+self.im_size, pos, self.im_rawmode)] - self.im_idat = len - raise EOFError - - def chunk_IEND(self, pos, len): - - # end of PNG image - raise EOFError - - def chunk_PLTE(self, pos, len): - - # palette - s = ImageFile._safe_read(self.fp, len) - if self.im_mode == "P": - self.im_palette = "RGB", s - return s - - def chunk_tRNS(self, pos, len): - - # transparency - s = ImageFile._safe_read(self.fp, len) - if self.im_mode == "P": - i = string.find(s, chr(0)) - if i >= 0: - self.im_info["transparency"] = i - elif self.im_mode == "L": - self.im_info["transparency"] = i16(s) - elif self.im_mode == "RGB": - self.im_info["transparency"] = i16(s), i16(s[2:]), i16(s[4:]) - return s - - def chunk_gAMA(self, pos, len): - - # gamma setting - s = ImageFile._safe_read(self.fp, len) - self.im_info["gamma"] = i32(s) / 100000.0 - return s - - def chunk_pHYs(self, pos, len): - - # pixels per unit - s = ImageFile._safe_read(self.fp, len) - px, py = i32(s), i32(s[4:]) - unit = ord(s[8]) - if unit == 1: # meter - dpi = int(px * 0.0254 + 0.5), int(py * 0.0254 + 0.5) - self.im_info["dpi"] = dpi - elif unit == 0: - self.im_info["aspect"] = px, py - return s - - def chunk_tEXt(self, pos, len): - - # text - s = ImageFile._safe_read(self.fp, len) - try: - k, v = string.split(s, "\0", 1) - except ValueError: - k = s; v = "" # fallback for broken tEXt tags - if k: - self.im_info[k] = self.im_text[k] = v - return s - - def chunk_zTXt(self, pos, len): - - # compressed text - s = ImageFile._safe_read(self.fp, len) - k, v = string.split(s, "\0", 1) - comp_method = ord(v[0]) - if comp_method != 0: - raise SyntaxError("Unknown compression method %s in zTXt chunk" % comp_method) - import zlib - self.im_info[k] = self.im_text[k] = zlib.decompress(v[1:]) - return s - -# -------------------------------------------------------------------- -# PNG reader - -def _accept(prefix): - return prefix[:8] == _MAGIC - -## -# Image plugin for PNG images. - -class PngImageFile(ImageFile.ImageFile): - - format = "PNG" - format_description = "Portable network graphics" - - def _open(self): - - if self.fp.read(8) != _MAGIC: - raise SyntaxError, "not a PNG file" - - # - # Parse headers up to the first IDAT chunk - - self.png = PngStream(self.fp) - - while 1: - - # - # get next chunk - - cid, pos, len = self.png.read() - - try: - s = self.png.call(cid, pos, len) - except EOFError: - break - except AttributeError: - if Image.DEBUG: - print cid, pos, len, "(unknown)" - s = ImageFile._safe_read(self.fp, len) - - self.png.crc(cid, s) - - # - # Copy relevant attributes from the PngStream. An alternative - # would be to let the PngStream class modify these attributes - # directly, but that introduces circular references which are - # difficult to break if things go wrong in the decoder... - # (believe me, I've tried ;-) - - self.mode = self.png.im_mode - self.size = self.png.im_size - self.info = self.png.im_info - self.text = self.png.im_text # experimental - self.tile = self.png.im_tile - - if self.png.im_palette: - rawmode, data = self.png.im_palette - self.palette = ImagePalette.raw(rawmode, data) - - self.__idat = len # used by load_read() - - - def verify(self): - "Verify PNG file" - - if self.fp is None: - raise RuntimeError("verify must be called directly after open") - - # back up to beginning of IDAT block - self.fp.seek(self.tile[0][2] - 8) - - self.png.verify() - self.png.close() - - self.fp = None - - def load_prepare(self): - "internal: prepare to read PNG file" - - if self.info.get("interlace"): - self.decoderconfig = self.decoderconfig + (1,) - - ImageFile.ImageFile.load_prepare(self) - - def load_read(self, bytes): - "internal: read more image data" - - while self.__idat == 0: - # end of chunk, skip forward to next one - - self.fp.read(4) # CRC - - cid, pos, len = self.png.read() - - if cid not in ["IDAT", "DDAT"]: - self.png.push(cid, pos, len) - return "" - - self.__idat = len # empty chunks are allowed - - # read more data from this chunk - if bytes <= 0: - bytes = self.__idat - else: - bytes = min(bytes, self.__idat) - - self.__idat = self.__idat - bytes - - return self.fp.read(bytes) - - - def load_end(self): - "internal: finished reading image data" - - self.png.close() - self.png = None - - -# -------------------------------------------------------------------- -# PNG writer - -def o16(i): - return chr(i>>8&255) + chr(i&255) - -def o32(i): - return chr(i>>24&255) + chr(i>>16&255) + chr(i>>8&255) + chr(i&255) - -_OUTMODES = { - # supported PIL modes, and corresponding rawmodes/bits/color combinations - "1": ("1", chr(1)+chr(0)), - "L;1": ("L;1", chr(1)+chr(0)), - "L;2": ("L;2", chr(2)+chr(0)), - "L;4": ("L;4", chr(4)+chr(0)), - "L": ("L", chr(8)+chr(0)), - "LA": ("LA", chr(8)+chr(4)), - "I": ("I;16B", chr(16)+chr(0)), - "P;1": ("P;1", chr(1)+chr(3)), - "P;2": ("P;2", chr(2)+chr(3)), - "P;4": ("P;4", chr(4)+chr(3)), - "P": ("P", chr(8)+chr(3)), - "RGB": ("RGB", chr(8)+chr(2)), - "RGBA":("RGBA", chr(8)+chr(6)), -} - -def putchunk(fp, cid, *data): - "Write a PNG chunk (including CRC field)" - - data = string.join(data, "") - - fp.write(o32(len(data)) + cid) - fp.write(data) - hi, lo = Image.core.crc32(data, Image.core.crc32(cid)) - fp.write(o16(hi) + o16(lo)) - -class _idat: - # wrap output from the encoder in IDAT chunks - - def __init__(self, fp, chunk): - self.fp = fp - self.chunk = chunk - def write(self, data): - self.chunk(self.fp, "IDAT", data) - -def _save(im, fp, filename, chunk=putchunk, check=0): - # save an image to disk (called by the save method) - - mode = im.mode - - if mode == "P": - - # - # attempt to minimize storage requirements for palette images - - if im.encoderinfo.has_key("bits"): - - # number of bits specified by user - n = 1 << im.encoderinfo["bits"] - - else: - - # check palette contents - n = 256 # FIXME - - if n <= 2: - bits = 1 - elif n <= 4: - bits = 2 - elif n <= 16: - bits = 4 - else: - bits = 8 - - if bits != 8: - mode = "%s;%d" % (mode, bits) - - # encoder options - if im.encoderinfo.has_key("dictionary"): - dictionary = im.encoderinfo["dictionary"] - else: - dictionary = "" - - im.encoderconfig = (im.encoderinfo.has_key("optimize"), dictionary) - - # get the corresponding PNG mode - try: - rawmode, mode = _OUTMODES[mode] - except KeyError: - raise IOError, "cannot write mode %s as PNG" % mode - - if check: - return check - - # - # write minimal PNG file - - fp.write(_MAGIC) - - chunk(fp, "IHDR", - o32(im.size[0]), o32(im.size[1]), # 0: size - mode, # 8: depth/type - chr(0), # 10: compression - chr(0), # 11: filter category - chr(0)) # 12: interlace flag - - if im.mode == "P": - chunk(fp, "PLTE", im.im.getpalette("RGB")) - - if im.encoderinfo.has_key("transparency"): - if im.mode == "P": - transparency = max(0, min(255, im.encoderinfo["transparency"])) - chunk(fp, "tRNS", chr(255) * transparency + chr(0)) - elif im.mode == "L": - transparency = max(0, min(65535, im.encoderinfo["transparency"])) - chunk(fp, "tRNS", o16(transparency)) - elif im.mode == "RGB": - red, green, blue = im.encoderinfo["transparency"] - chunk(fp, "tRNS", o16(red) + o16(green) + o16(blue)) - else: - raise IOError("cannot use transparency for this mode") - - if 0: - # FIXME: to be supported some day - chunk(fp, "gAMA", o32(int(gamma * 100000.0))) - - dpi = im.encoderinfo.get("dpi") - if dpi: - chunk(fp, "pHYs", - o32(int(dpi[0] / 0.0254 + 0.5)), - o32(int(dpi[1] / 0.0254 + 0.5)), - chr(1)) - - info = im.encoderinfo.get("pnginfo") - if info: - for cid, data in info.chunks: - chunk(fp, cid, data) - - # ICC profile writing support -- 2008-06-06 Florian Hoech - if im.info.has_key("icc_profile"): - # ICC profile - # according to PNG spec, the iCCP chunk contains: - # Profile name 1-79 bytes (character string) - # Null separator 1 byte (null character) - # Compression method 1 byte (0) - # Compressed profile n bytes (zlib with deflate compression) - try: - import ICCProfile - p = ICCProfile.ICCProfile(im.info["icc_profile"]) - name = p.tags.desc.get("ASCII", p.tags.desc.get("Unicode", p.tags.desc.get("Macintosh", p.tags.desc.get("en", {}).get("US", "ICC Profile")))).encode("latin1", "replace")[:79] - except ImportError: - name = "ICC Profile" - data = name + "\0\0" + zlib.compress(im.info["icc_profile"]) - chunk(fp, "iCCP", data) - - ImageFile._save(im, _idat(fp, chunk), [("zip", (0,0)+im.size, 0, rawmode)]) - - chunk(fp, "IEND", "") - - try: - fp.flush() - except: - pass - - -# -------------------------------------------------------------------- -# PNG chunk converter - -def getchunks(im, **params): - """Return a list of PNG chunks representing this image.""" - - class collector: - data = [] - def write(self, data): - pass - def append(self, chunk): - self.data.append(chunk) - - def append(fp, cid, *data): - data = string.join(data, "") - hi, lo = Image.core.crc32(data, Image.core.crc32(cid)) - crc = o16(hi) + o16(lo) - fp.append((cid, data, crc)) - - fp = collector() - - try: - im.encoderinfo = params - _save(im, fp, None, append) - finally: - del im.encoderinfo - - return fp.data - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open("PNG", PngImageFile, _accept) -Image.register_save("PNG", _save) - -Image.register_extension("PNG", ".png") - -Image.register_mime("PNG", "image/png") diff --git a/image_occlusion_enhanced/Imaging/PIL/PpmImagePlugin.py b/image_occlusion_enhanced/Imaging/PIL/PpmImagePlugin.py deleted file mode 100644 index e86146c1..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/PpmImagePlugin.py +++ /dev/null @@ -1,131 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PPM support for PIL -# -# History: -# 96-03-24 fl Created -# 98-03-06 fl Write RGBA images (as RGB, that is) -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# - - -__version__ = "0.2" - -import string - -import Image, ImageFile - -# -# -------------------------------------------------------------------- - -MODES = { - # standard - "P4": "1", - "P5": "L", - "P6": "RGB", - # extensions - "P0CMYK": "CMYK", - # PIL extensions (for test purposes only) - "PyP": "P", - "PyRGBA": "RGBA", - "PyCMYK": "CMYK" -} - -def _accept(prefix): - return prefix[0] == "P" and prefix[1] in "0456y" - -## -# Image plugin for PBM, PGM, and PPM images. - -class PpmImageFile(ImageFile.ImageFile): - - format = "PPM" - format_description = "Pbmplus image" - - def _token(self, s = ""): - while 1: # read until next whitespace - c = self.fp.read(1) - if not c or c in string.whitespace: - break - s = s + c - return s - - def _open(self): - - # check magic - s = self.fp.read(1) - if s != "P": - raise SyntaxError, "not a PPM file" - mode = MODES[self._token(s)] - - if mode == "1": - self.mode = "1" - rawmode = "1;I" - else: - self.mode = rawmode = mode - - for ix in range(3): - while 1: - while 1: - s = self.fp.read(1) - if s not in string.whitespace: - break - if s != "#": - break - s = self.fp.readline() - s = int(self._token(s)) - if ix == 0: - xsize = s - elif ix == 1: - ysize = s - if mode == "1": - break - - self.size = xsize, ysize - self.tile = [("raw", - (0, 0, xsize, ysize), - self.fp.tell(), - (rawmode, 0, 1))] - - # ALTERNATIVE: load via builtin debug function - # self.im = Image.core.open_ppm(self.filename) - # self.mode = self.im.mode - # self.size = self.im.size - -# -# -------------------------------------------------------------------- - -def _save(im, fp, filename): - if im.mode == "1": - rawmode, head = "1;I", "P4" - elif im.mode == "L": - rawmode, head = "L", "P5" - elif im.mode == "RGB": - rawmode, head = "RGB", "P6" - elif im.mode == "RGBA": - rawmode, head = "RGB", "P6" - else: - raise IOError, "cannot write mode %s as PPM" % im.mode - fp.write(head + "\n%d %d\n" % im.size) - if head != "P4": - fp.write("255\n") - ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, 0, 1))]) - - # ALTERNATIVE: save via builtin debug function - # im._dump(filename) - -# -# -------------------------------------------------------------------- - -Image.register_open("PPM", PpmImageFile, _accept) -Image.register_save("PPM", _save) - -Image.register_extension("PPM", ".pbm") -Image.register_extension("PPM", ".pgm") -Image.register_extension("PPM", ".ppm") diff --git a/image_occlusion_enhanced/Imaging/PIL/__init__.py b/image_occlusion_enhanced/Imaging/PIL/__init__.py deleted file mode 100644 index ed54d26f..00000000 --- a/image_occlusion_enhanced/Imaging/PIL/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# package placeholder -# -# Copyright (c) 1999 by Secret Labs AB. -# -# See the README file for information on usage and redistribution. -# - -# ;-) diff --git a/image_occlusion_enhanced/Imaging/README b/image_occlusion_enhanced/Imaging/README deleted file mode 100644 index 458975b3..00000000 --- a/image_occlusion_enhanced/Imaging/README +++ /dev/null @@ -1,300 +0,0 @@ -The Python Imaging Library -$Id$ - -Release 1.1.7 (November 15, 2009) - -==================================================================== -The Python Imaging Library 1.1.7 -==================================================================== - -Contents --------- - -+ Introduction -+ Support Options - - Commercial support - - Free support -+ Software License -+ Build instructions (all platforms) - - Additional notes for Mac OS X - - Additional notes for Windows - --------------------------------------------------------------------- -Introduction --------------------------------------------------------------------- - -The Python Imaging Library (PIL) adds image processing capabilities -to your Python environment. This library provides extensive file -format support, an efficient internal representation, and powerful -image processing capabilities. - -This source kit has been built and tested with Python 2.0 and newer, -on Windows, Mac OS X, and major Unix platforms. Large parts of the -library also work on 1.5.2 and 1.6. - -The main distribution site for this software is: - - http://www.pythonware.com/products/pil/ - -That site also contains information about free and commercial support -options, PIL add-ons, answers to frequently asked questions, and more. - - -Development versions (alphas, betas) are available here: - - http://effbot.org/downloads/ - - -The PIL handbook is not included in this distribution; to get the -latest version, check: - - http://www.pythonware.com/library/ - http://effbot.org/books/imagingbook/ (drafts) - - -For installation and licensing details, see below. - - --------------------------------------------------------------------- -Support Options --------------------------------------------------------------------- - -+ Commercial Support - -Secret Labs (PythonWare) offers support contracts for companies using -the Python Imaging Library in commercial applications, and in mission- -critical environments. The support contract includes technical support, -bug fixes, extensions to the PIL library, sample applications, and more. - -For the full story, check: - - http://www.pythonware.com/products/pil/support.htm - - -+ Free Support - -For support and general questions on the Python Imaging Library, send -e-mail to the Image SIG mailing list: - - image-sig@python.org - -You can join the Image SIG by sending a mail to: - - image-sig-request@python.org - -Put "subscribe" in the message body to automatically subscribe to the -list, or "help" to get additional information. Alternatively, you can -send your questions to the Python mailing list, python-list@python.org, -or post them to the newsgroup comp.lang.python. DO NOT SEND SUPPORT -QUESTIONS TO PYTHONWARE ADDRESSES. - - --------------------------------------------------------------------- -Software License --------------------------------------------------------------------- - -The Python Imaging Library is - -Copyright (c) 1997-2009 by Secret Labs AB -Copyright (c) 1995-2009 by Fredrik Lundh - -By obtaining, using, and/or copying this software and/or its -associated documentation, you agree that you have read, understood, -and will comply with the following terms and conditions: - -Permission to use, copy, modify, and distribute this software and its -associated documentation for any purpose and without fee is hereby -granted, provided that the above copyright notice appears in all -copies, and that both that copyright notice and this permission notice -appear in supporting documentation, and that the name of Secret Labs -AB or the author not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior -permission. - -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO -THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR -ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - --------------------------------------------------------------------- -Build instructions (all platforms) --------------------------------------------------------------------- - -For a list of changes in this release, see the CHANGES document. - -0. If you're in a hurry, try this: - - $ tar xvfz Imaging-1.1.7.tar.gz - $ cd Imaging-1.1.7 - $ python setup.py install - - If you prefer to know what you're doing, read on. - - -1. Prerequisites. - - If you need any of the features described below, make sure you - have the necessary libraries before building PIL. - - feature library - ----------------------------------------------------------------- - JPEG support libjpeg (6a or 6b) - - http://www.ijg.org - http://www.ijg.org/files/jpegsrc.v6b.tar.gz - ftp://ftp.uu.net/graphics/jpeg/ - - PNG support zlib (1.2.3 or later is recommended) - - http://www.gzip.org/zlib/ - - OpenType/TrueType freetype2 (2.3.9 or later is recommended) - support - http://www.freetype.org - http://freetype.sourceforge.net - - CMS support littleCMS (1.1.5 or later is recommended) - support - http://www.littlecms.com/ - - If you have a recent Linux version, the libraries provided with the - operating system usually work just fine. If some library is - missing, installing a prebuilt version (jpeg-devel, zlib-devel, - etc) is usually easier than building from source. For example, for - Ubuntu 9.10 (karmic), you can install the following libraries: - - sudo apt-get install libjpeg62-dev - sudo apt-get install zlib1g-dev - sudo apt-get install libfreetype6-dev - sudo apt-get install liblcms1-dev - - If you're using Mac OS X, you can use the 'fink' tool to install - missing libraries (also see the Mac OS X section below). - - Similar tools are available for many other platforms. - - -2. To build under Python 1.5.2, you need to install the stand-alone - version of the distutils library: - - http://www.python.org/sigs/distutils-sig/download.html - - You can fetch distutils 1.0.2 from the Python source repository: - - svn export http://svn.python.org/projects/python/tags/Distutils-1_0_2/Lib/distutils/ - - For newer releases, the distutils library is included in the - Python standard library. - - NOTE: Version 1.1.7 is not fully compatible with 1.5.2. Some - more recent additions to the library may not work, but the core - functionality is available. - - -3. If you didn't build Python from sources, make sure you have - Python's build support files on your machine. If you've down- - loaded a prebuilt package (e.g. a Linux RPM), you probably - need additional developer packages. Look for packages named - "python-dev", "python-devel", or similar. For example, for - Ubuntu 9.10 (karmic), use the following command: - - sudo apt-get install python-dev - - -4. When you have everything you need, unpack the PIL distribution - (the file Imaging-1.1.7.tar.gz) in a suitable work directory: - - $ cd MyExtensions # example - $ gunzip Imaging-1.1.7.tar.gz - $ tar xvf Imaging-1.1.7.tar - - -5. Build the library. We recommend that you do an in-place build, - and run the self test before installing. - - $ cd Imaging-1.1.7 - $ python setup.py build_ext -i - $ python selftest.py - - During the build process, the setup.py will display a summary - report that lists what external components it found. The self- - test will display a similar report, with what external components - the tests found in the actual build files: - - ---------------------------------------------------------------- - PIL 1.1.7 SETUP SUMMARY - ---------------------------------------------------------------- - *** TKINTER support not available (Tcl/Tk 8.5 libraries needed) - --- JPEG support available - --- ZLIB (PNG/ZIP) support available - --- FREETYPE support available - ---------------------------------------------------------------- - - Make sure that the optional components you need are included. - - If the build script won't find a given component, you can edit the - setup.py file and set the appropriate ROOT variable. For details, - see instructions in the file. - - If the build script finds the component, but the tests cannot - identify it, try rebuilding *all* modules: - - $ python setup.py clean - $ python setup.py build_ext -i - - -6. If the setup.py and selftest.py commands finish without any - errors, you're ready to install the library: - - $ python setup.py install - - (depending on how Python has been installed on your machine, - you might have to log in as a superuser to run the 'install' - command, or use the 'sudo' command to run 'install'.) - - --------------------------------------------------------------------- -Additional notes for Mac OS X --------------------------------------------------------------------- - -On Mac OS X you will usually install additional software such as -libjpeg or freetype with the "fink" tool, and then it ends up in -"/sw". If you have installed the libraries elsewhere, you may have -to tweak the "setup.py" file before building. - - --------------------------------------------------------------------- -Additional notes for Windows --------------------------------------------------------------------- - -On Windows, you need to tweak the ROOT settings in the "setup.py" -file, to make it find the external libraries. See comments in the -file for details. - -Make sure to build PIL and the external libraries with the same -runtime linking options as was used for the Python interpreter -(usually /MD, under Visual Studio). - - -Note that most Python distributions for Windows include libraries -compiled for Microsoft Visual Studio. You can get the free Express -edition of Visual Studio from: - - http://www.microsoft.com/Express/ - -To build extensions using other tool chains, see the "Using -non-Microsoft compilers on Windows" section in the distutils handbook: - - http://www.python.org/doc/current/inst/non-ms-compilers.html - -For additional information on how to build extensions using the -popular MinGW compiler, see: - - http://mingw.org (compiler) - http://sebsauvage.net/python/mingw.html (build instructions) - http://sourceforge.net/projects/gnuwin32 (prebuilt libraries) - diff --git a/image_occlusion_enhanced/LICENSE b/image_occlusion_enhanced/LICENSE deleted file mode 100644 index dba13ed2..00000000 --- a/image_occlusion_enhanced/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/image_occlusion_enhanced/__init__.py b/image_occlusion_enhanced/__init__.py deleted file mode 100644 index 7c68785e..00000000 --- a/image_occlusion_enhanced/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/image_occlusion_enhanced/add.py b/image_occlusion_enhanced/add.py deleted file mode 100644 index b04cf738..00000000 --- a/image_occlusion_enhanced/add.py +++ /dev/null @@ -1,353 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -## Based on Image Occlusion 2.0 ## -## Copyright (c) 2012-2015 tmbb ## -## (https://github.com/tmbb) ## -## ## -#################################################### - -""" -Add notes. -""" - -import os -import tempfile - -from aqt.qt import * - -from aqt import mw -from aqt.utils import getFile, tooltip - -from ngen import * -from config import * - -from editor import ImgOccEdit -from dialogs import ioError -from utils import imageProp, img2path, path2url - -# SVG-Edit configuration -svg_edit_dir = os.path.join(os.path.dirname(__file__), - 'svg-edit', - 'svg-edit-2.6') -svg_edit_path = os.path.join(svg_edit_dir, - 'svg-editor.html') -svg_edit_ext = "ext-image-occlusion.js,ext-arrows.js,\ -ext-markers.js,ext-shapes.js,ext-eyedropper.js,ext-panning.js,\ -ext-snapping.js" -svg_edit_fonts = "'Helvetica LT Std', Arial, sans-serif" -svg_edit_queryitems = [('initStroke[opacity]', '1'), - ('showRulers', 'false'), - ('extensions', svg_edit_ext)] - -class ImgOccAdd(object): - def __init__(self, editor, origin, oldimg=None): - self.ed = editor - self.image_path = oldimg - self.mode = "add" - self.origin = origin - self.opref = {} # original io session preference - loadConfig(self) - - def occlude(self, image_path=None): - - note = self.ed.note - isIO = (note and note.model() == mw.col.models.byName(IO_MODEL_NAME)) - - if not image_path: - if self.origin == "addcards": - image_path = self.getNewImage(parent=self.ed.parentWindow) - if not image_path: - return False - elif isIO: - msg, image_path = self.getIONoteData(note) - self.mode = "edit" - if not image_path: - tooltip(msg) - return False - else: - image_path = self.getImageFromFields(note.fields) - if image_path: - tooltip("Non-editable note.
" - "Using image to create new IO note.") - - if not image_path: - tooltip(("This note cannot be edited, nor is there
" - "an image to use for an image occlusion.")) - return False - - self.setPreservedAttrs(note) - self.image_path = image_path - - width, height = imageProp(image_path) - if not width: - tooltip("Not a valid image file.") - return False - - self.callImgOccEdit(width, height) - - - def setPreservedAttrs(self, note): - self.opref["tags"] = self.ed.tags.text() - if self.origin == "addcards": - self.opref["did"] = self.ed.parentWindow.deckChooser.selectedId() - else: - self.opref["did"] = mw.col.db.scalar( - "select did from cards where id = ?", note.cards()[0].id) - - - def getIONoteData(self, note): - """Select image based on mode and set original field contents""" - - note_id = note[self.ioflds['id']] - image_path = img2path(note[self.ioflds['im']]) - omask = img2path(note[self.ioflds['om']]) - - if note_id == None or note_id.count("-") != 2: - msg = "Editing unavailable: Invalid image occlusion Note ID" - return msg, None - elif not omask or not image_path: - msg = "Editing unavailable: Missing image or original mask" - return msg, None - - note_id_grps = note_id.split('-') - self.opref["note_id"] = note_id - self.opref["uniq_id"] = note_id_grps[0] - self.opref["occl_tp"] = note_id_grps[1] - self.opref["image"] = image_path - self.opref["omask"] = omask - - return None, image_path - - - def getImageFromFields(self, fields): - """Parse fields for valid images""" - image_path = None - for fld in fields: - image_path = img2path(fld) - if image_path: - break - return image_path - - - def getNewImage(self, parent=None, noclip=False): - """Get image from file selection or clipboard""" - if noclip: - clip = None - else: - clip = QApplication.clipboard() - if clip and clip.mimeData().imageData(): - handle, image_path = tempfile.mkstemp(suffix='.png') - clip.image().save(image_path) - clip.clear() - if os.stat(image_path).st_size == 0: - # workaround for a clipboard bug - return self.getNewImage(noclip=True) - else: - return unicode(image_path) - - # retrieve last used image directory - prev_image_dir = self.lconf["dir"] - if not prev_image_dir or not os.path.isdir(prev_image_dir): - prev_image_dir = IO_HOME - - image_path = QFileDialog.getOpenFileName(parent, - "Select an Image", prev_image_dir, - "Image Files (*.png *jpg *.jpeg *.gif)") - image_path = unicode(image_path) - - if not image_path: - return None - elif not os.path.isfile(image_path): - tooltip("Invalid image file path") - return False - else: - self.lconf["dir"] = os.path.dirname(image_path) - return image_path - - - def callImgOccEdit(self, width, height): - """Set up variables, call and prepare ImgOccEdit""" - ofill = self.sconf['ofill'] - scol = self.sconf['scol'] - swidth = self.sconf['swidth'] - fsize = self.sconf['fsize'] - font = self.sconf['font'] - - bkgd_url = path2url(self.image_path) - opref = self.opref - onote = self.ed.note - flds = self.mflds - deck = mw.col.decks.nameOrNone(opref["did"]) - - try: - mw.ImgOccEdit is not None - mw.ImgOccEdit.resetWindow() - # use existing IO instance when available - except AttributeError: - mw.ImgOccEdit = ImgOccEdit(mw) - mw.ImgOccEdit.setupFields(flds) - logging.debug("Launching new ImgOccEdit instance") - dialog = mw.ImgOccEdit - dialog.switchToMode(self.mode) - - url = QUrl.fromLocalFile(svg_edit_path) - url.setQueryItems(svg_edit_queryitems) - url.addQueryItem('initFill[color]', ofill) - url.addQueryItem('dimensions', '{0},{1}'.format(width, height)) - url.addQueryItem('bkgd_url', bkgd_url) - url.addQueryItem('initStroke[color]', scol) - url.addQueryItem('initStroke[width]', str(swidth)) - url.addQueryItem('text[font_size]', str(fsize)) - url.addQueryItem('text[font_family]', "'%s', %s" % (font, svg_edit_fonts)) - - if self.mode != "add": - url.addQueryItem('initTool', 'select'), - for i in flds: - fn = i["name"] - if fn in self.ioflds_priv: - continue - dialog.tedit[fn].setPlainText(onote[fn].replace('
', '\n')) - svg_url = path2url(opref["omask"]) - url.addQueryItem('url', svg_url) - else: - url.addQueryItem('initTool', 'rect'), - - dialog.svg_edit.setUrl(url) - dialog.deckChooser.deck.setText(deck) - dialog.tags_edit.setCol(mw.col) - dialog.tags_edit.setText(opref["tags"]) - - if onote: - for i in self.ioflds_prsv: - if i in onote: - dialog.tedit[i].setPlainText(onote[i]) - - dialog.visible = True - if self.mode == "add": - dialog.show() - else: - # modal dialog when editing - dialog.exec_() - - - def onChangeImage(self): - """Change canvas background image""" - image_path = self.getNewImage() - if not image_path: - return False - width, height = imageProp(image_path) - if not width: - tooltip("Not a valid image file.") - return False - bkgd_url = path2url(image_path) - mw.ImgOccEdit.svg_edit.eval(""" - svgCanvas.setBackground('#FFF', '%s'); - svgCanvas.setResolution(%s, %s); - //svgCanvas.zoomChanged('', 'canvas'); - """ %(bkgd_url, width, height)) - self.image_path = image_path - - - def onAddNotesButton(self, choice, close): - """Get occlusion settings in and pass them to the note generator (add)""" - dialog = mw.ImgOccEdit - svg_edit = dialog.svg_edit - svg = svg_edit.page().mainFrame().evaluateJavaScript( - "svgCanvas.svgCanvasToString();") - svg = unicode(svg) # store svg as unicode string - - r1 = self.getUserInputs(dialog) - if r1 == False: - return False - (fields, tags) = r1 - did = dialog.deckChooser.selectedId() - - noteGenerator = genByKey(choice) - gen = noteGenerator(self.ed, svg, self.image_path, - self.opref, tags, fields, did) - r = gen.generateNotes() - if r == False: - return False - - if self.origin == "addcards" and self.ed.note: - # Update Editor with modified tags and sources field - self.ed.tags.setText(" ".join(tags)) - self.ed.saveTags() - for i in self.ioflds_prsv: - if i in self.ed.note: - self.ed.note[i] = fields[i] - self.ed.loadNote() - deck = mw.col.decks.nameOrNone(did) - self.ed.parentWindow.deckChooser.deck.setText(deck) - - if close: - dialog.close() - - mw.reset() - - - def onEditNotesButton(self, choice): - """Get occlusion settings and pass them to the note generator (edit)""" - dialog = mw.ImgOccEdit - svg_edit = dialog.svg_edit - svg = svg_edit.page().mainFrame().evaluateJavaScript( - "svgCanvas.svgCanvasToString();") - - r1 = self.getUserInputs(dialog, edit=True) - if r1 == False: - return False - (fields, tags) = r1 - did = self.opref["did"] - old_occl_tp = self.opref["occl_tp"] - - noteGenerator = genByKey(choice, old_occl_tp) - gen = noteGenerator(self.ed, svg, self.image_path, - self.opref, tags, fields, did) - r = gen.updateNotes() - if r == False: - return False - - mw.ImgOccEdit.close() - - if r == "reset": - # modifications to mask require media collection reset - ## refresh webview image cache - QWebSettings.clearMemoryCaches() - ## write a dummy file to update collection.media modtime and force sync - media_dir = mw.col.media.dir() - fpath = os.path.join(media_dir, "syncdummy.txt") - if not os.path.isfile(fpath): - with open(fpath, "w") as f: - f.write("io sync dummy") - os.remove(fpath) - - mw.reset() # FIXME: causes glitches in editcurrent mode - - - def getUserInputs(self, dialog, edit=False): - """Get fields and tags from ImgOccEdit while checking note type""" - fields = {} - # note type integrity check: - io_model_fields = mw.col.models.fieldNames(self.model) - if not all(x in io_model_fields for x in self.ioflds.values()): - ioError("Error: Image Occlusion note type " \ - "not configured properly.Please make sure you did not " \ - "manually delete or rename any of the default fields.", - help="notetype") - return False - for i in self.mflds: - fn = i['name'] - if fn in self.ioflds_priv: - continue - if edit and fn in self.sconf["skip"]: - continue - text = dialog.tedit[fn].toPlainText().replace('\n', '
') - fields[fn] = text - tags = dialog.tags_edit.text().split() - return (fields, tags) diff --git a/image_occlusion_enhanced/config.py b/image_occlusion_enhanced/config.py deleted file mode 100644 index 9c69f50d..00000000 --- a/image_occlusion_enhanced/config.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -#################################################### - -""" -Sets up configuration, including constants -""" - -import os -import sys - -from aqt import mw - -global IO_FLDS, IO_FLDS_IDS -global IO_MODEL_NAME, IO_CARD_NAME, IO_HOME, IO_HOTKEY - -IO_MODEL_NAME = "Image Occlusion Enhanced" -IO_CARD_NAME = "IO Card" - -IO_FLDS = { - 'id': u"ID (hidden)", - 'hd': u"Header", - 'im': u"Image", - 'ft': u"Footer", - 'rk': u"Remarks", - 'sc': u"Sources", - 'e1': u"Extra 1", - 'e2': u"Extra 2", - 'qm': u"Question Mask", - 'am': u"Answer Mask", - 'om': u"Original Mask" -} - -IO_FLDS_IDS = ["id", "hd", "im", "qm", "ft", "rk", - "sc", "e1", "e2", "am", "om"] - -# TODO: Use IDs instead of names to make these compatible with self.ioflds - -# fields that aren't user-editable -IO_FIDS_PRIV = ['id', 'im', 'qm', 'am', 'om'] - -# fields that are synced between an IO Editor session and Anki's Editor -IO_FIDS_PRSV = ['sc'] - -# variables for local preference handling -sys_encoding = sys.getfilesystemencoding() -IO_HOME = os.path.expanduser('~').decode(sys_encoding) -IO_HOTKEY = "Ctrl+Shift+O" - -# default configurations -default_conf_local = {'version': 1.01, - 'dir': IO_HOME, - 'hotkey': IO_HOTKEY} -default_conf_syncd = {'version': 1.01, - 'ofill': 'FFEBA2', - 'qfill': 'FF7E7E', - 'scol': '2D2D2D', - 'swidth': 3, - 'font': 'Arial', - 'fsize': 24, - 'skip': [IO_FLDS["e1"], IO_FLDS["e2"]], - 'flds': IO_FLDS} - -import template - -def loadConfig(self): - """load and/or create add-on preferences""" - # Synced preferences - if not 'imgocc' in mw.col.conf: - # create initial configuration - mw.col.conf['imgocc'] = default_conf_syncd - - # upgrade from earlier IO versions: - if 'image_occlusion_conf' in mw.col.conf: - old_conf = mw.col.conf['image_occlusion_conf'] - mw.col.conf['imgocc']['ofill'] = old_conf['initFill[color]'] - mw.col.conf['imgocc']['qfill'] = old_conf['mask_fill_color'] - # insert other upgrade actions here - mw.col.setMod() - - elif mw.col.conf['imgocc']['version'] < default_conf_syncd['version']: - print "Updating config DB from earlier IO release" - for key in default_conf_syncd.keys(): - if key not in mw.col.conf['imgocc']: - mw.col.conf['imgocc'][key] = default_conf_syncd[key] - mw.col.conf['imgocc']['version'] = default_conf_syncd['version'] - # insert other update actions here: - # template.update_template(mw.col) # update card templates - mw.col.setMod() - - - # Local preferences - if not 'imgocc' in mw.pm.profile: - mw.pm.profile["imgocc"] = default_conf_local - elif mw.pm.profile['imgocc'].get('version', 0) < default_conf_syncd['version']: - for key in default_conf_local.keys(): - if key not in mw.col.conf['imgocc']: - mw.pm.profile["imgocc"][key] = default_conf_local[key] - mw.pm.profile['imgocc']['version'] = default_conf_local['version'] - - model = mw.col.models.byName(IO_MODEL_NAME) - if not model: - # create model and set up default field name config - model = template.add_io_model(mw.col) - mw.col.conf['imgocc']['flds'] = default_conf_syncd['flds'] - mflds = model['flds'] - ioflds = mw.col.conf['imgocc']['flds'] - ioflds_priv = [] - for i in IO_FIDS_PRIV: - ioflds_priv.append(ioflds[i]) - # preserve fields if they are marked as sticky in the IO note type: - ioflds_prsv = [] - for fld in mflds: - fname = fld['name'] - if fld['sticky'] and fname not in ioflds_priv: - ioflds_prsv.append(fname) - - self.sconf_dflt = default_conf_syncd - self.lconf_dflt = default_conf_local - self.sconf = mw.col.conf['imgocc'] - self.lconf = mw.pm.profile["imgocc"] - - self.ioflds = ioflds - self.ioflds_priv = ioflds_priv - self.ioflds_prsv = ioflds_prsv - self.model = model - self.mflds = mflds diff --git a/image_occlusion_enhanced/dialogs.py b/image_occlusion_enhanced/dialogs.py deleted file mode 100644 index 96f120db..00000000 --- a/image_occlusion_enhanced/dialogs.py +++ /dev/null @@ -1,172 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -## Based on Image Occlusion 2.0 ## -## Copyright (c) 2012-2015 tmbb ## -## (https://github.com/tmbb) ## -## ## -#################################################### - -""" -Handles all minor utility dialogs -""" - -import logging, sys - -from PyQt4 import QtCore, QtGui -from aqt.qt import * -from anki.errors import AnkiError - -from aqt import mw, webview, deckchooser, tagedit -from aqt.utils import saveGeom, restoreGeom - -from config import * -from resources import * - - -def ioError(text, title="Image Occlusion Enhanced Error", - parent=None, help=None): - msgfunc = QMessageBox.critical - btns = None - if help: - btns = QMessageBox.Help | QMessageBox.Ok - while 1: - r = ioInfo(text, title, parent, buttons=btns, msgfunc=msgfunc) - if r == QMessageBox.Help: - ioHelp(help, parent=parent) - else: - break - return r - -def ioAskUser(text, title="Image Occlusion Enhanced", parent=None, - help="", defaultno=False, msgfunc=None): - """Show a yes/no question. Return true if yes. - based on askUser by Damien Elmes""" - - msgfunc = QMessageBox.question - btns = QMessageBox.Yes | QMessageBox.No - if help: - btns |= QMessageBox.Help - while 1: - if defaultno: - default = QMessageBox.No - else: - default = QMessageBox.Yes - r = ioInfo(text, title, parent, btns, default, msgfunc) - if r == QMessageBox.Help: - ioHelp(help) - else: - break - return r == QMessageBox.Yes - -def ioInfo(text, title="Image Occlusion Enhanced", parent=None, - buttons=None, default=None, msgfunc=None): - if not parent: - parent = mw.app.activeWindow() - if not buttons: - buttons = QMessageBox.Ok - if not default: - default = QMessageBox.Ok - if not msgfunc: - msgfunc = QMessageBox.information - return msgfunc(parent, title, text, buttons, default) - - -io_link_wiki = "https://github.com/Glutanimate/image-occlusion-enhanced/wiki" -io_link_tut = "https://www.youtube.com/playlist?list=PL3MozITKTz5YFHDGB19ypxcYfJ1ITk_6o" -io_link_thread = ("https://anki.tenderapp.com/discussions/add-ons/" - "8295-image-occlusion-enhanced-official-thread") -help_text = {} -help_text["add"] = u""" -

Basic Instructions

-
    -
  1. With the rectangle tool or any other shape tool selected, cover the areas of the image you want to be tested on
  2. -
  3. (Optional): Fill out additional information about your cards by switching to the Fields tab
  4. -
  5. Click on one of the Add Cards buttons at the bottom of the window to add the cards to your collection
  6. -
-

Drawing Custom Labels

-
    -
  1. Draw up the layers sidepanel by clicking on the Layers button at the right edge of the editor
  2. -
  3. Switch to the Labels layer by left-clicking on it. You can also switch to the labels layer directly by using Ctrl + Shift + L.
  4. -
  5. Anything you draw in this layer – be it text, lines, or shapes – will appear above the image, but still below your masks. All of the painting tools in the left sidebar are at your disposal.
  6. -
  7. Switch back to the masks layer, either via the Layers sidepanel, or by using the Ctrl + Shift + M hotkey.
  8. -
-

Grouping Shapes

-
    -
  1. Select multiple shapes, either by drawing a selection rectangle with the selection tool active (S), or by shift-clicking on multiple shapes
  2. -
  3. Either use the G hotkey or the Group Elements tool in the top-bar to group your items
  4. -
-

Grouped shapes will form a single card.

-

More Information

-

For more information please refer to the following resources:

- - """.format(io_link_wiki, io_link_tut, io_link_thread) -help_text["edit"] = """Instructions for editing: \ -

Each mask shape represents a card.\ - Removing any of the existing shapes will remove the corresponding card.\ - New shapes will generate new cards. You can change the occlusion type\ - by using the dropdown box on the left.

If you click on the \ - Add new cards button a completely new batch of cards will be \ - generated, leaving your originals untouched.

\ - Actions performed in Image Occlusion's Editing Mode cannot be\ - easily undone, so please make sure to check your changes twice before\ - applying them.

The only exception to this are purely textual\ - changes to fields like the header or footer of your notes. These can\ - be fully reverted by using Ctrl+Z in the Browser or Reviewer view.

\ - More information: Wiki: Editing Notes.\ - """ % (io_link_wiki + "/Basic-Use#editing-cards") -help_text["notetype"] = """Fixing a broken note type:\ -

The Image Occlusion Enhanced note type can't be edited \ - arbitrarily. If you delete a field that's required by the add-on \ - or rename it outside of the IO Options dialog you will be presented \ - with an error message.

- To fix this issue please follow the instructions in the \ - wiki.""" % (io_link_wiki + "/Troubleshooting#note-type") -help_text["main"] = u"""

Help and Support

-

Image Occlusion Enhanced Wiki

-

Official Video Tutorial Series

-

Support Thread

-

Credits and License

-

Copyright © 2012-2015 \ - Tiago Barroso

-

Copyright © 2013 \ - Steve AW

-

Copyright © 2016-2017 \ - Aristotelis P.

-

Image Occlusion Enhanced is licensed under the GNU AGPLv3.

-

Third-party open-source software shipped with Image Occlusion Enhanced:

-
  • SVG Edit 2.6. \ - Copyright (c) 2009-2012 SVG-edit authors. Licensed under the MIT license

  • -
  • Python Imaging Library \ - (PIL) 1.1.7. Copyright (c) 1997-2011 by Secret Labs AB, Copyright (c) 1995-2011 by Fredrik \ - Lundh. Licensed under the \ - PIL license

  • -
  • imagesize.py v0.7.1. \ - Copyright (c) 2016 Yoshiki Shibukawa. Licensed under the MIT license.

  • -
- """ % (io_link_wiki, io_link_tut, io_link_thread) - -def ioHelp(help, title=None, text=None, parent=None): - """Display an info message or a predefined help section""" - if help != "custom": - text = help_text[help] - if not title: - title = "Image Occlusion Enhanced Help" - if not parent: - parent = mw.app.activeWindow() - mbox = QMessageBox(parent) - mbox.setAttribute(Qt.WA_DeleteOnClose) - mbox.setStandardButtons(QMessageBox.Ok) - mbox.setWindowTitle(title) - mbox.setText(text) - mbox.setWindowModality(Qt.NonModal) - mbox.show() \ No newline at end of file diff --git a/image_occlusion_enhanced/editor.py b/image_occlusion_enhanced/editor.py deleted file mode 100644 index 492a52d3..00000000 --- a/image_occlusion_enhanced/editor.py +++ /dev/null @@ -1,364 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -## Based on Image Occlusion 2.0 ## -## Copyright (c) 2012-2015 tmbb ## -## (https://github.com/tmbb) ## -## ## -#################################################### - -""" -Image Occlusion editor dialog -""" - -from PyQt4 import QtCore, QtGui -from aqt.qt import * - -from aqt import mw, webview, deckchooser, tagedit -from aqt.utils import saveGeom, restoreGeom - -from dialogs import ioHelp -from config import * - -class ImgOccEdit(QDialog): - """Main Image Occlusion Editor dialog""" - def __init__(self, mw): - QDialog.__init__(self, parent=None) - self.setWindowFlags(Qt.Window) - self.visible = False - self.mode = "add" - loadConfig(self) - self.setupUi() - restoreGeom(self, "imgoccedit") - - def closeEvent(self, event): - if mw.pm.profile is not None: - self.deckChooser.cleanup() - saveGeom(self, "imgoccedit") - self.visible = False - event.accept() - - def reject(self): - # Override QDialog Esc key reject - pass - - def setupUi(self): - """Set up ImgOccEdit UI""" - # Main widgets aside from fields - self.svg_edit = webview.AnkiWebView() - # required for local href links to work properly (e.g. context menu): - self.svg_edit.page().setLinkDelegationPolicy(QWebPage.DelegateExternalLinks) - self.svg_edit.setCanFocus(True) # focus necessary for hotkeys - self.tags_hbox = QHBoxLayout() - self.tags_edit = tagedit.TagEdit(self) - self.tags_label = QLabel("Tags") - self.tags_label.setFixedWidth(70) - self.deck_container = QWidget() - self.deckChooser = deckchooser.DeckChooser(mw, - self.deck_container, label=True) - self.deckChooser.deck.setAutoDefault(False) - - # workaround for tab focus order issue of the tags entry - # (this particular section is only needed when the quick deck - # buttons add-on is installed) - if self.deck_container.layout().children(): # multiple deck buttons - for i in range(self.deck_container.layout().children()[0].count()): - try: - item = self.deck_container.layout().children()[0].itemAt(i) - # remove Tab focus manually: - item.widget().setFocusPolicy(Qt.ClickFocus) - item.widget().setAutoDefault(False) - except AttributeError: - pass - - - # Button row widgets - self.bottom_label = QLabel() - button_box = QtGui.QDialogButtonBox(QtCore.Qt.Horizontal, self) - button_box.setCenterButtons(False) - - image_btn = QPushButton("Change &Image", clicked=self.changeImage) - image_btn.setIcon(QIcon(":/icons/new_occlusion.png")) - image_btn.setIconSize(QSize(16, 16)) - image_btn.setAutoDefault(False) - help_btn = QPushButton("&Help", clicked=self.onHelp) - help_btn.setAutoDefault(False) - - self.occl_tp_select = QComboBox() - self.occl_tp_select.addItems(["Don't Change", "Hide All, Reveal One", - "Hide One, Reveal All"]) - - self.edit_btn = button_box.addButton("&Edit Cards", - QDialogButtonBox.ActionRole) - self.new_btn = button_box.addButton("&Add New Cards", - QDialogButtonBox.ActionRole) - self.ao_btn = button_box.addButton(u"Hide &All, Reveal One", - QDialogButtonBox.ActionRole) - self.oa_btn = button_box.addButton(u"Hide &One, Reveal All", - QDialogButtonBox.ActionRole) - close_button = button_box.addButton("&Close", - QDialogButtonBox.RejectRole) - - image_tt = ("Switch to a different image while preserving all of " - "the shapes and fields") - dc_tt = "Preserve existing occlusion type" - edit_tt = "Edit all cards using current mask shapes and field entries" - new_tt = "Create new batch of cards without editing existing ones" - ao_tt = ("Generate cards with nonoverlapping information, where all
" - "labels are hidden on the front and one revealed on the back") - oa_tt = ("Generate cards with overlapping information, where one
" - "label is hidden on the front and revealed on the back") - close_tt = "Close Image Occlusion Editor without generating cards" - - image_btn.setToolTip(image_tt) - self.edit_btn.setToolTip(edit_tt) - self.new_btn.setToolTip(new_tt) - self.ao_btn.setToolTip(ao_tt) - self.oa_btn.setToolTip(oa_tt) - close_button.setToolTip(close_tt) - self.occl_tp_select.setItemData(0, dc_tt, Qt.ToolTipRole) - self.occl_tp_select.setItemData(1, ao_tt, Qt.ToolTipRole) - self.occl_tp_select.setItemData(2, oa_tt, Qt.ToolTipRole) - - for btn in [image_btn, self.edit_btn, self.new_btn, self.ao_btn, - self.oa_btn, close_button]: - btn.setFocusPolicy(Qt.ClickFocus) - - self.connect(self.edit_btn, SIGNAL("clicked()"), self.editNote) - self.connect(self.new_btn, SIGNAL("clicked()"), self.new) - self.connect(self.ao_btn, SIGNAL("clicked()"), self.addAO) - self.connect(self.oa_btn, SIGNAL("clicked()"), self.addOA) - self.connect(close_button, SIGNAL("clicked()"), self.close) - - # Set basic layout up - - ## Button row - bottom_hbox = QHBoxLayout() - bottom_hbox.addWidget(image_btn) - bottom_hbox.addWidget(help_btn) - bottom_hbox.insertStretch(2, stretch=1) - bottom_hbox.addWidget(self.bottom_label) - bottom_hbox.addWidget(self.occl_tp_select) - bottom_hbox.addWidget(button_box) - - ## Tab 1 - vbox1 = QVBoxLayout() - vbox1.addWidget(self.svg_edit, stretch=1) - - ## Tab 2 - self.vbox2 = QVBoxLayout() - # vbox2 fields are variable and added by setupFields() at a later point - - ## Main Tab Widget - tab1 = QWidget() - self.tab2 = QWidget() - tab1.setLayout(vbox1) - self.tab2.setLayout(self.vbox2) - self.tab_widget = QtGui.QTabWidget() - self.tab_widget.setFocusPolicy(Qt.ClickFocus) - self.tab_widget.addTab(tab1,"&Masks Editor") - self.tab_widget.addTab(self.tab2,"&Fields") - self.tab_widget.setTabToolTip(1, "Include additional information (optional)") - self.tab_widget.setTabToolTip(0, "Create image occlusion masks (required)") - - ## Main Window - vbox_main = QVBoxLayout() - vbox_main.setMargin(5) - vbox_main.addWidget(self.tab_widget) - vbox_main.addLayout(bottom_hbox) - self.setLayout(vbox_main) - self.setMinimumWidth(640) - self.tab_widget.setCurrentIndex(0) - self.svg_edit.setFocus() - - # Define and connect key bindings - - ## Field focus hotkeys - for i in range(1,10): - s = self.connect(QtGui.QShortcut(QtGui.QKeySequence("Ctrl+%i" %i), self), - QtCore.SIGNAL('activated()'), - lambda f=i-1:self.focusField(f)) - ## Other hotkeys - self.connect(QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Return"), self), - QtCore.SIGNAL('activated()'), lambda: self.defaultAction(True)) - self.connect(QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Shift+Return"), self), - QtCore.SIGNAL('activated()'), lambda: self.addOA(True)) - self.connect(QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Tab"), self), - QtCore.SIGNAL('activated()'), self.switchTabs) - self.connect(QtGui.QShortcut(QtGui.QKeySequence("Ctrl+r"), self), - QtCore.SIGNAL('activated()'), self.resetMainFields) - self.connect(QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Shift+r"), self), - QtCore.SIGNAL('activated()'), self.resetAllFields) - self.connect(QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Shift+t"), self), - QtCore.SIGNAL('activated()'), self.focusTags) - self.connect(QtGui.QShortcut(QtGui.QKeySequence("Ctrl+f"), self), - QtCore.SIGNAL('activated()'), self.fitImageCanvas) - - - # Various actions that act on / interact with the ImgOccEdit UI: - - # Note actions - - def changeImage(self): - mw.ImgOccAdd.onChangeImage() - def defaultAction(self, close): - if self.mode == "add": - self.addAO(close) - else: - self.editNote() - def addAO(self, close=False): - mw.ImgOccAdd.onAddNotesButton("ao", close) - def addOA(self, close=False): - mw.ImgOccAdd.onAddNotesButton("oa", close) - def new(self, close=False): - choice = self.occl_tp_select.currentText() - mw.ImgOccAdd.onAddNotesButton(choice, close) - def editNote(self): - choice = self.occl_tp_select.currentText() - mw.ImgOccAdd.onEditNotesButton(choice) - def onHelp(self): - if self.mode == "add": - ioHelp("add") - else: - ioHelp("edit") - - - # Window state - - def resetFields(self): - """Reset all widgets. Needed for changes to the note type""" - layout = self.vbox2 - for i in reversed(range(layout.count())): - item = layout.takeAt(i) - layout.removeItem(item) - if item.widget(): - item.widget().setParent(None) - elif item.layout(): - sublayout = item.layout() - sublayout.setParent(None) - for i in reversed(range(sublayout.count())): - subitem = sublayout.takeAt(i) - sublayout.removeItem(subitem) - subitem.widget().setParent(None) - self.tags_hbox.setParent(None) - - def resetWindow(self): - """Fully reset window state""" - self.resetAllFields() - self.tab_widget.setCurrentIndex(0) - self.occl_tp_select.setCurrentIndex(0) - self.tedit[self.ioflds["hd"]].setFocus() - self.svg_edit.setFocus() - - def setupFields(self, flds): - """Setup dialog text edits based on note type fields""" - self.tedit = {} - self.tlabel = {} - self.flds = flds - for i in flds: - if i['name'] in self.ioflds_priv: - continue - hbox = QHBoxLayout() - tedit = QPlainTextEdit() - label = QLabel(i["name"]) - hbox.addWidget(label) - hbox.addWidget(tedit) - tedit.setTabChangesFocus(True) - tedit.setMinimumHeight(40) - label.setFixedWidth(70) - self.tedit[i["name"]] = tedit - self.tlabel[i["name"]] = label - self.vbox2.addLayout(hbox) - - self.tags_hbox.addWidget(self.tags_label) - self.tags_hbox.addWidget(self.tags_edit) - self.vbox2.addLayout(self.tags_hbox) - self.vbox2.addWidget(self.deck_container) - # switch Tab focus order of deckchooser and tags_edit ( - # for some reason it's the wrong way around by default): - self.tab2.setTabOrder(self.tags_edit, self.deckChooser.deck) - - def switchToMode(self, mode): - """Toggle between add and edit layouts""" - hide_on_add = [self.occl_tp_select, self.edit_btn, self.new_btn] - hide_on_edit = [self.ao_btn, self.oa_btn] - self.mode = mode - for i in self.tedit.values(): - i.show() - for i in self.tlabel.values(): - i.show() - if mode == "add": - for i in hide_on_add: - i.hide() - for i in hide_on_edit: - i.show() - dl_txt = "Deck" - ttl = "Image Occlusion Enhanced - Add Mode" - bl_txt = "Add Cards:" - else: - for i in hide_on_add: - i.show() - for i in hide_on_edit: - i.hide() - for i in self.sconf['skip']: - if i in self.tedit.keys(): - self.tedit[i].hide() - self.tlabel[i].hide() - dl_txt = "Deck for Add new cards" - ttl = "Image Occlusion Enhanced - Editing Mode" - bl_txt = "Type:" - self.deckChooser.deckLabel.setText(dl_txt) - self.setWindowTitle(ttl) - self.bottom_label.setText(bl_txt) - - # Other actions - - def switchTabs(self): - currentTab = self.tab_widget.currentIndex() - if currentTab == 0: - self.tab_widget.setCurrentIndex(1) - if isinstance(QApplication.focusWidget(), QPushButton): - self.tedit[self.ioflds["hd"]].setFocus() - else: - self.tab_widget.setCurrentIndex(0) - - def focusField(self, idx): - """Focus field in vbox2 layout by index number""" - self.tab_widget.setCurrentIndex(1) - target_item = self.vbox2.itemAt(idx) - if not target_item: - return - target_layout = target_item.layout() - target_widget = target_item.widget() - if target_layout: - target = target_layout.itemAt(1).widget() - elif target_widget: - target = target_widget - target.setFocus() - - def focusTags(self): - self.tab_widget.setCurrentIndex(1) - self.tags_edit.setFocus() - - def resetMainFields(self): - """Reset all fields aside from sticky ones""" - for i in self.flds: - fn = i['name'] - if fn in self.ioflds_priv or fn in self.ioflds_prsv: - continue - self.tedit[fn].setPlainText("") - - def resetAllFields(self): - """Reset all fields""" - self.resetMainFields() - for i in self.ioflds_prsv: - self.tedit[i].setPlainText("") - - def fitImageCanvas(self): - command = "svgCanvas.zoomChanged('', 'canvas');" - self.svg_edit.eval(command) diff --git a/image_occlusion_enhanced/main.py b/image_occlusion_enhanced/main.py deleted file mode 100644 index 4de1c695..00000000 --- a/image_occlusion_enhanced/main.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -## Based on Image Occlusion 2.0 ## -## Copyright (c) 2012-2015 tmbb ## -## (https://github.com/tmbb) ## -## ## -#################################################### - -""" -Sets up buttons and menus and calls other modules. -""" - -import logging, sys - -from anki.lang import _ -from aqt.qt import * - -from aqt import mw -from aqt.editor import Editor, EditorWebView -from aqt.addcards import AddCards -from aqt.editcurrent import EditCurrent -from aqt.reviewer import Reviewer -from aqt.utils import tooltip -from anki.hooks import wrap, addHook, runHook - -from config import * -from resources import * -from add import ImgOccAdd -from options import ImgOccOpts -from dialogs import ioHelp, ioError - -logging.basicConfig(stream=sys.stdout, level=logging.ERROR) - - -def onIoSettings(mw): - """Call settings dialog if Editor not active""" - if hasattr(mw, "ImgOccEdit") and mw.ImgOccEdit.visible: - tooltip("Please close Image Occlusion Editor\ - to access the Options.") - return - dialog = ImgOccOpts(mw) - dialog.exec_() - - -def onIoHelp(): - """Call main help dialog""" - ioHelp("main") - - -def onImgOccButton(self, origin=None, image_path=None): - """Launch Image Occlusion Enhanced""" - origin = origin or getEdParentInstance(self.parentWindow) - io_model = mw.col.models.byName(IO_MODEL_NAME) - if io_model: - io_model_fields = mw.col.models.fieldNames(io_model) - if "imgocc" in mw.col.conf: - dflt_fields = mw.col.conf['imgocc']['flds'].values() - else: - dflt_fields = IO_FLDS.values() - # note type integrity check - if not all(x in io_model_fields for x in dflt_fields): - ioError("Error: Image Occlusion note type " \ - "not configured properly.Please make sure you did not " \ - "manually delete or rename any of the default fields.", - help="notetype") - return False - try: # allows us to fall back to old image if necessary - oldimg = mw.ImgOccAdd.image_path - except AttributeError: - oldimg = None - mw.ImgOccAdd = ImgOccAdd(self, origin, oldimg) - mw.ImgOccAdd.occlude(image_path) - - -def onSetupEditorButtons(self): - """Add IO button to Editor""" - conf = mw.pm.profile.get("imgocc") - if not conf: - hotkey = IO_HOTKEY - else: - hotkey = conf.get("hotkey", IO_HOTKEY) - - origin = getEdParentInstance(self.parentWindow) - - if origin == "addcards": - tt = "Add Image Occlusion" - icon = "new_occlusion" - else: - tt = "Edit Image Occlusion" - icon = "new_occlusion" - - btn = self._addButton(icon, lambda o=self: onImgOccButton(self, origin), - _(hotkey), _(u"{} ({})".format(tt, hotkey)), canDisable=False) - - -def getEdParentInstance(parent): - """Determine parent instance of editor widget""" - if isinstance(parent, AddCards): - return "addcards" - elif isinstance(parent, EditCurrent): - return "editcurrent" - else: - return "browser" - - -def openImage(path): - """Open path with default system app""" - import subprocess - try: - if sys.platform=='win32': - os.startfile(path) - elif sys.platform=='darwin': - subprocess.Popen(['open', path]) - else: - subprocess.Popen(['xdg-open', path]) - except OSError: - QDesktopServices.openUrl(QUrl("file://" + path)) - - -def contextMenuEvent(self, evt): - """Add custom context menu for images""" - m = QMenu(self) - a = m.addAction(_("Cut")) - a.triggered.connect(self.onCut) - a = m.addAction(_("Copy")) - a.triggered.connect(self.onCopy) - a = m.addAction(_("Paste")) - a.triggered.connect(self.onPaste) - ################################################## - hit = self.page().currentFrame().hitTestContent(evt.pos()) - url = hit.imageUrl() - if url.isValid(): - image_url = url.toLocalFile() - a = m.addAction(_("Occlude Image")) - a.triggered.connect( - lambda _, u=image_url, s=self.editor: onImgOccButton( - s, image_path=u)) - a = m.addAction(_("Open Image")) - a.triggered.connect(lambda _, u=image_url: openImage(u)) - ################################################## - runHook("EditorWebView.contextMenuEvent", self, m) - m.popup(QCursor.pos()) - - -def onSetNote(self, note, hide=True, focus=False): - """Customize the editor when IO notes are active""" - if not (self.note and self.note.model()["name"] == IO_MODEL_NAME): - return - # simple hack to hide the ID field if it's the first one - if self.note.model()['flds'][0]['name'] == IO_FLDS['id']: - self.web.eval(""" - // hide first fname, field, and snowflake (FrozenFields add-on) - document.styleSheets[0].addRule( - 'tr:first-child .fname, #f0, #i0', 'display: none;'); - """) - # Limit image display height - self.web.eval(""" - document.styleSheets[0].addRule( - 'img', 'max-width: 90%; max-height: 160px'); - """) - - -def newKeyHandler(self, evt): - """Bind mask reveal to a hotkey""" - if (self.state == "answer" and evt.key() == Qt.Key_G): - self.web.eval('document.getElementById("io-revl-btn").click();') - - -def onShowAnswer(self, _old): - """Retain scroll position across answering the card""" - if not self.card or not self.card.model()["name"] == IO_MODEL_NAME: - return _old(self) - scroll_pos = self.web.page().mainFrame().scrollPosition() - ret = _old(self) - self.web.page().mainFrame().setScrollPosition(scroll_pos) - return ret - - -# Set up menus -options_action = QAction("Image &Occlusion Enhanced Options...", mw) -help_action = QAction("Image &Occlusion Enhanced...", mw) -mw.connect(options_action, SIGNAL("triggered()"), - lambda o=mw: onIoSettings(o)) -mw.connect(help_action, SIGNAL("triggered()"), - onIoHelp) -mw.form.menuTools.addAction(options_action) -mw.form.menuHelp.addAction(help_action) - -# Set up hooks -addHook('setupEditorButtons', onSetupEditorButtons) -EditorWebView.contextMenuEvent = contextMenuEvent -Editor.setNote = wrap(Editor.setNote, onSetNote, "after") -Editor.onImgOccButton = onImgOccButton -Reviewer._keyHandler = wrap(Reviewer._keyHandler, newKeyHandler, "before") -Reviewer._showAnswer = wrap(Reviewer._showAnswer, onShowAnswer, "around") \ No newline at end of file diff --git a/image_occlusion_enhanced/nconvert.py b/image_occlusion_enhanced/nconvert.py deleted file mode 100644 index 837f53d3..00000000 --- a/image_occlusion_enhanced/nconvert.py +++ /dev/null @@ -1,262 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -#################################################### - -""" -Makes older IO notes editable. -""" - -import logging, sys - -from PyQt4.QtCore import SIGNAL -from PyQt4.QtGui import QKeySequence -from anki.hooks import addHook -from aqt.utils import tooltip - -from xml.dom import minidom - -from config import * -from dialogs import ioAskUser -from utils import img2path, fname2img - -class ImgOccNoteConverter(object): - def __init__(self, browser): - self.browser = browser - self.occl_id_last = None - loadConfig(self) - - def convertNotes(self, nids): - """Main note conversion method""" - nids_by_nr = {} - skipped = 0 - (io_nids, filtered) = self.filterSelected(nids) - for nid in io_nids: - note = mw.col.getNote(nid) - (uniq_id, note_nr) = self.getDataFromNamingScheme(note) - if uniq_id == False: - logging.debug("Skipping note that couldn't be parsed: %s", nid) - skipped += 1 - continue - occl_tp = self.getOcclTypeAndNodes(note) - occl_id = uniq_id + '-' + occl_tp - if occl_id == self.occl_id_last: - logging.debug("Skipping note that we've just converted: %s", nid) - continue - self.occl_id_last = occl_id - for nid in self.findByNoteId(uniq_id): - note = mw.col.getNote(nid) - (uniq_id, note_nr) = self.getDataFromNamingScheme(note) - if uniq_id == False: - logging.debug("Skipping note that couldn't be parsed: %s", nid) - skipped += 1 - continue - nids_by_nr[int(note_nr)] = nid - self.idAndCorrelateNotes(nids_by_nr, occl_id) - converted = len(io_nids) - tooltip("%i notes updated, %i skipped" - % (converted - skipped, filtered + skipped)) - - def filterSelected(self, nids): - """Filters out notes with the wrong note type and those that are - valid already""" - io_nids = [] - filtered = 0 - for nid in nids: - note = mw.col.getNote(nid) - if note.model() != self.model: - logging.debug("Skipping note with wrong note type: %s", nid) - filtered +=1 - continue - elif note[self.ioflds['id']]: - logging.debug("Skipping IO note that is already editable: %s", nid) - filtered +=1 - continue - elif not note[self.ioflds['om']]: - logging.debug("Skipping IO note without original SVG mask: %s", nid) - filtered +=1 - continue - logging.debug("Found IO note in need of update: %s", nid) - io_nids.append(nid) - return (io_nids, filtered) - - def findByNoteId(self, note_id): - """Search collection for notes with given ID in their omask paths""" - # need to use omask path because Note ID field is not yet set - query = "'%s':'*%s*'" % ( self.ioflds['om'], note_id ) - logging.debug("query: %s", query) - res = mw.col.findNotes(query) - return res - - def getDataFromNamingScheme(self, note): - """Get unique ID and note nr from qmask path""" - qmask = note[self.ioflds['qm']] - path = img2path(qmask, True) - if not path: - return (False, None) - grps = path.split('_') - try: - if len(grps) == 2: - logging.debug("Extracting data using IO 2.0 naming scheme") - uniq_id = grps[0] - note_nr = path.split(' ')[1].split('.')[0] - else: - logging.debug("Extracting data using IO Enhanced naming scheme") - grps = path.split('-') - uniq_id = grps[0] - note_nr = int(grps[2]) - 1 - return (uniq_id, note_nr) - except IndexError: - return (False, None) - - - def idAndCorrelateNotes(self, nids_by_nr, occl_id): - """Update Note ID fields and omasks of all occlusion session siblings""" - logging.debug("occl_id %s", occl_id) - logging.debug("nids_by_nr %s", nids_by_nr) - logging.debug("mnode_idxs %s", self.mnode_idxs) - - for nr in sorted(nids_by_nr.keys()): - try: - midx = self.mnode_idxs[nr] - except IndexError: - continue - nid = nids_by_nr[nr] - note = mw.col.getNote(nid) - new_mnode_id = occl_id + '-' + str(nr+1) - self.mnode.childNodes[midx].setAttribute("id", new_mnode_id) - note[self.ioflds['id']] = new_mnode_id - note.flush() - logging.debug("Adding ID for note nr %s", nr) - logging.debug("midx %s", midx) - logging.debug("nid %s", nid) - logging.debug("note %s", note) - logging.debug("new_mnode_id %s", new_mnode_id) - - new_svg = self.svg_node.toxml() - omask_path = self._saveMask(new_svg, occl_id, "O") - logging.debug("omask_path %s", omask_path) - - for nid in nids_by_nr.values(): - note = mw.col.getNote(nid) - note[self.ioflds['om']] = fname2img(omask_path) - note.addTag(".io-converted") - note.flush() - logging.debug("Setting om and tag for nid %s", nid) - - def getOcclTypeAndNodes(self, note): - """Determine oclusion type and svg mask nodes""" - nr_of_masks = {} - mnode_idxs = {} - svg_mlayer = {} - for i in ["qm", "om"]: # om second, so that end vars are correct - svg_file = img2path(note[self.ioflds[i]], True) - svg_node= self.readSvg(svg_file) - svg_mlayer = self.layerNodesFrom(svg_node)[-1] # topmost layer - mnode_idxs = self.getMaskNodes(svg_mlayer) - nr_of_masks[i] = len(mnode_idxs) - # decide on occl_tp based on nr of mask nodes in omask vs qmask - if nr_of_masks["om"] != nr_of_masks["qm"]: - occl_tp = "oa" - else: - occl_tp = "ao" - self.svg_node = svg_node - self.mnode = svg_mlayer - self.mnode_idxs = mnode_idxs - return occl_tp - - def readSvg(self, svg_file): - """Read and fix malformatted IO 2.0 SVGs""" - svg_doc = minidom.parse(svg_file) - # ugly workaround for wrong namespace in older IO notes: - svg_string = svg_doc.toxml().replace('ns0:', '').replace(':ns0','') - svg_string = unicode(svg_string) - svg_doc = minidom.parseString(svg_string.encode('utf-8')) - svg_node = svg_doc.documentElement - return svg_node - - def getMaskNodes(self, mlayer): - """Find mask nodes in masks layer""" - mnode_indexes = [] - for i, node in enumerate(mlayer.childNodes): - if (node.nodeType == node.ELEMENT_NODE) and (node.nodeName != 'title'): - mnode_indexes.append(i) - return mnode_indexes - - def layerNodesFrom(self, svg_node): - """Get layer nodes (topmost group nodes below the SVG node)""" - assert (svg_node.nodeType == svg_node.ELEMENT_NODE) - assert (svg_node.nodeName == 'svg') - layer_nodes = [node for node in svg_node.childNodes - if node.nodeType == node.ELEMENT_NODE] - assert (len(layer_nodes) >= 1) - # last, i.e. top-most element, needs to be a layer: - assert (layer_nodes[-1].nodeName == 'g') - return layer_nodes - - def _saveMask(self, mask, note_id, mtype): - """Write mask to file in media collection""" - logging.debug("!saving %s, %s", note_id, mtype) - mask_path = '%s-%s.svg' % (note_id, mtype) - mask_file = open(mask_path, 'w') - mask_file.write(mask.encode('utf-8')) - mask_file.close() - return mask_path - -def onIoConvert(self): - """Launch initial dialog, set up checkpoint, invoke converter""" - mw = self.mw - selected = self.selectedNotes() - if not selected: - tooltip("No cards selected.", period=2000) - return - question = u"""\ - This is a purely experimental feature that is meant to update older \ - IO notes to be compatible with the new editing feature-set in IO Enhanced. \ - Clicking on 'Yes' below will prompt the add-on to go through all selected \ - notes and change their Note ID and mask files in a way that should make it \ - possible to edit them in the future. \ -

Please note that this will only work for notes \ - that have already been switched to the Image Occlusion Enhanced note type.\ - If you are coming from IO 2.0 or an older version of IO Enhanced you will \ - first have to switch the note type of your notes manually by going to Edit → \ - Change Note Type.

\ - WARNING: There is no guarantee that this feature will actually succeed in \ - updating your notes properly. To convert legacy notes the add-on will have to \ - make a few assumptions which in some rare instances might turn out to be wrong \ - and lead to broken notes. Notes that can't be parsed for the information needed \ - to convert into an editable state (e.g. a valid "Original Mask" field) will usually \ - be skipped by the add-on, but there might be some corner cases where that won't work. \ -

A checkpoint will be set to revert to if needed, \ - but even with that safety measure in place you should still only use this \ - function if you know what you are doing.\ -

Continue anyway?
(Depending on the number of notes this might \ - take a while) - """ - ret = ioAskUser(question, "Please confirm action", self, defaultno=True) - if not ret: - return False - mw.progress.start() - mw.checkpoint("Image Occlusion Note Conversions") - self.model.beginReset() - conv = ImgOccNoteConverter(self) - conv.convertNotes(selected) - self.model.endReset() - mw.col.reset() - mw.reset() - mw.progress.finish() - -# Set up menus and hooks - -def setupMenu(self): - menu = self.form.menuEdit - menu.addSeparator() - a = menu.addAction('Convert to Editable IO &Enhanced Notes') - self.connect(a, SIGNAL("triggered()"), lambda b=self: onIoConvert(b)) - -addHook("browser.setupMenus", setupMenu) diff --git a/image_occlusion_enhanced/ngen.py b/image_occlusion_enhanced/ngen.py deleted file mode 100644 index b04bbf1c..00000000 --- a/image_occlusion_enhanced/ngen.py +++ /dev/null @@ -1,469 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -## Based on Simple Picture Occlusion ## -## Copyright (c) 2013 SteveAW ## -## (https://github.com/steveaw) ## -## ## -#################################################### - -""" -Generates the actual IO notes and writes them to -the collection. -""" - -import os -import logging, sys - -from aqt.qt import * -from aqt import mw -from aqt.utils import tooltip -from anki.notes import Note - -from xml.dom import minidom -from uuid import uuid - -from dialogs import ioHelp, ioAskUser -from utils import fname2img -from config import * - -# Explanation of some of the variables: -# -# nid: Note ID set by Anki -# note_id: Image Occlusion Note ID set as the first field of each IO note -# uniq_id: Unique sequence of random characters. First part of the note_id -# occl_tp: Two-letter code that signifies occlusion type. Second part of -# the note_id -# occl_id: Combination of uniq_id + occl_tp - unique identifier shared -# by all notes created in one IO session -# note_nr: Third part of the note_id - -def genByKey(key, old_occl_tp=None): - """Get note generator based on occl_tp/user input""" - if key in ["Don't Change"]: - return genByKey(old_occl_tp, None) - elif key in ["ao", "Hide All, Reveal One"]: - return IoGenHideAllRevealOne - elif key in ["oa", "Hide One, Reveal All"]: - return IoGenHideOneRevealAll - else: - return IoGenHideAllRevealOne - -class ImgOccNoteGenerator(object): - """Generic note generator object""" - - stripattr = ['opacity', 'stroke-opacity', 'fill-opacity'] - - def __init__(self, ed, svg, image_path, opref, tags, fields, did): - self.ed = ed - self.new_svg = svg - self.image_path = image_path - self.opref = opref - self.tags = tags - self.fields = fields - self.did = did - self.qfill = '#' + mw.col.conf['imgocc']['qfill'] - loadConfig(self) - - def generateNotes(self): - """Generate new notes""" - state = "default" - self.uniq_id = str(uuid.uuid4()).replace("-","") - self.occl_id = '%s-%s' % (self.uniq_id, self.occl_tp) - - ( svg_node, layer_node ) = self._getMnodesAndSetIds() - if not self.mnode_ids: - tooltip("No cards to generate.
\ - Are you sure you set your masks correctly?") - return False - - self.new_svg = svg_node.toxml() # write changes to svg - omask_path = self._saveMask(self.new_svg, self.occl_id, "O") - qmasks = self._generateMaskSVGsFor("Q") - amasks = self._generateMaskSVGsFor("A") - image_path = mw.col.media.addFile(self.image_path) - img = fname2img(image_path) - - mw.checkpoint("Adding Image Occlusion Cards") - for nr, idx in enumerate(self.mnode_indexes): - note_id = self.mnode_ids[idx] - self._saveMaskAndReturnNote(omask_path, qmasks[nr], amasks[nr], - img, note_id) - tooltip("%s %s added" % self._cardS(len(qmasks)), parent=None) - return state - - def updateNotes(self): - """Update existing notes""" - state = "default" - self.uniq_id = self.opref['uniq_id'] - self.occl_id = '%s-%s' % (self.uniq_id, self.occl_tp) - omask_path = None - - self._findAllNotes() - ( svg_node, mlayer_node ) = self._getMnodesAndSetIds(True) - if not self.mnode_ids: - tooltip("No shapes left. You can't delete all cards.
\ - Are you sure you set your masks correctly?") - return False - mw.checkpoint("Editing Image Occlusion Cards") - ret = self._deleteAndIdNotes(mlayer_node) - if not ret: - # confirmation window rejected - return False - else: - (del_count, new_count) = ret - - self.new_svg = svg_node.toxml() # write changes to svg - old_svg = self._getOriginalSvg() # load original svg - if self.new_svg != old_svg or self.occl_tp != self.opref["occl_tp"]: - # updated masks or occlusion type - omask_path = self._saveMask(self.new_svg, self.occl_id, "O") - qmasks = self._generateMaskSVGsFor("Q") - amasks = self._generateMaskSVGsFor("A") - state = "reset" - - image_path = mw.col.media.addFile(self.image_path) - img = fname2img(image_path) - - logging.debug("mnode_indexes %s", self.mnode_indexes) - for nr, idx in enumerate(self.mnode_indexes): - logging.debug("=====================") - logging.debug("nr %s", nr) - logging.debug("idx %s", idx) - note_id = self.mnode_ids[idx] - logging.debug("note_id %s", note_id) - logging.debug("self.nids %s", self.nids) - nid = self.nids[note_id] - logging.debug("nid %s", nid) - if omask_path: - self._saveMaskAndReturnNote(omask_path, qmasks[nr], amasks[nr], - img, note_id, nid) - else: - self._saveMaskAndReturnNote(None, None, None, - img, note_id, nid) - self._showUpdateTooltip(del_count, new_count) - return state - - def _cardS(self, cnt): - s = "card" - if cnt > 1 or cnt == 0: - s = "cards" - return (cnt, s) - - def _showUpdateTooltip(self, del_count, new_count): - upd_count = max(0, len(self.mnode_indexes) - del_count - new_count) - ttip = "%s old %s edited in place" % self._cardS(upd_count) - if del_count > 0: - ttip += "
%s existing %s deleted" % self._cardS(del_count) - if new_count > 0: - ttip += "
%s new %s created" % self._cardS(new_count) - tooltip(ttip, parent=self.ed.parentWindow) - - def _getOriginalSvg(self): - """Returns original SVG as a string""" - mask_doc = minidom.parse(self.opref["omask"]) - svg_node = mask_doc.documentElement - return svg_node.toxml() - - def _layerNodesFrom(self, svg_node): - """Get layer nodes (topmost group nodes below the SVG node)""" - assert (svg_node.nodeType == svg_node.ELEMENT_NODE) - assert (svg_node.nodeName == 'svg') - layer_nodes = [node for node in svg_node.childNodes - if node.nodeType == node.ELEMENT_NODE] - assert (len(layer_nodes) >= 1) - # last, i.e. top-most element, needs to be a layer: - assert (layer_nodes[-1].nodeName == 'g') - return layer_nodes - - def _getMnodesAndSetIds(self, edit=False): - """Find mask nodes in masks layer and read/set node IDs""" - self.mnode_indexes = [] - self.mnode_ids = {} - mask_doc = minidom.parseString(self.new_svg.encode('utf-8')) - svg_node = mask_doc.documentElement - cheight = float(svg_node.attributes["height"].value) - cwidth = float(svg_node.attributes["width"].value) - carea = cheight * cwidth - layer_nodes = self._layerNodesFrom(svg_node) - mlayer_node = layer_nodes[-1] # treat topmost layer as masks layer - - shift = 0 - for i, mnode in enumerate(mlayer_node.childNodes): - # minidom doesn't offer a childElements method and childNodes - # also returns whitespace found in the mlayer_node as a child node. - # For that reason we use self.mnode_indexes to register all - # indexes of mlayer_node children that contain actual elements, - # i.e. mask nodes - if (mnode.nodeType == mnode.ELEMENT_NODE) and (mnode.nodeName != 'title'): - i -= shift - if not edit and mnode.nodeName == "rect": - # remove microscopical shapes (usually accidentally drawn) - h_attr = mnode.attributes.get("height", 0) - w_attr = mnode.attributes.get("width", 0) - height = h_attr if not h_attr else float(mnode.attributes["height"].value) - width = w_attr if not w_attr else float(mnode.attributes["width"].value) - if not height or not width or 100*(height*width)/carea <= 0.01: - mlayer_node.removeChild(mnode) - shift += 1 - continue - self.mnode_indexes.append(i) - self._removeAttribsRecursively(mnode, self.stripattr) - if mnode.nodeName == "g": - # remove IDs of grouped shapes to prevent duplicates down the line - for node in mnode.childNodes: - self._removeAttribsRecursively(node, ["id"]) - if not edit: - self.mnode_ids[i] = "%s-%i" %(self.occl_id, len(self.mnode_indexes)) - mnode.setAttribute("id", self.mnode_ids[i]) - else: - self.mnode_ids[i] = mnode.attributes["id"].value - - return (svg_node, mlayer_node) - - def _findByNoteId(self, note_id): - """Search collection for notes with given ID""" - query = "'%s':'%s*'" % ( self.ioflds['id'], note_id ) - logging.debug("query %s", query) - res = mw.col.findNotes(query) - return res - - def _findAllNotes(self): - """Get matching nids by ID""" - old_occl_id = '%s-%s' % (self.uniq_id, self.opref["occl_tp"]) - res = self._findByNoteId(old_occl_id) - self.nids = {} - for nid in res: - note_id = mw.col.getNote(nid)[self.ioflds['id']] - self.nids[note_id] = nid - logging.debug('--------------------') - logging.debug("res %s", res) - logging.debug("nids %s", self.nids) - - def _deleteAndIdNotes(self, mlayer_node): - """ - Determine which mask nodes have been deleted or newly created and, depending - on which, either delete their respective notes or ID them in correspondence - with the numbering of older nodes - """ - uniq_id = self.opref['uniq_id'] - mnode_ids = self.mnode_ids - nids = self.nids - - # look for missing shapes by note_id - valid_mnode_note_ids = filter (lambda x:x.startswith(uniq_id), mnode_ids.values()) - valid_nid_note_ids = filter (lambda x:x.startswith(uniq_id), nids.keys()) - # filter out notes that have already been deleted manually - exstg_mnode_note_ids = [x for x in valid_mnode_note_ids if x in valid_nid_note_ids] - exstg_mnode_note_nrs = sorted([int(i.split('-')[-1]) for i in exstg_mnode_note_ids]) - # determine available nrs available for note numbering - if not exstg_mnode_note_nrs: - # only the case if the user deletes all existing shapes - max_mnode_note_nr = 0 - full_range = None - available_nrs = None - else: - max_mnode_note_nr = int(exstg_mnode_note_nrs[-1]) - full_range = range(1, max_mnode_note_nr+1) - available_nrs = set(full_range) - set(exstg_mnode_note_nrs) - available_nrs = sorted(list(available_nrs)) - - # compare note_ids as present in note collection with masks on svg - deleted_note_ids = set(valid_nid_note_ids) - set(valid_mnode_note_ids) - deleted_note_ids = sorted(list(deleted_note_ids)) - del_count = len(deleted_note_ids) - # set notes of missing masks on svg to be deleted - deleted_nids = [nids[x] for x in deleted_note_ids] - - logging.debug('--------------------') - logging.debug("valid_mnode_note_ids %s", valid_mnode_note_ids) - logging.debug("exstg_mnode_note_nrs %s", exstg_mnode_note_nrs) - logging.debug("max_mnode_note_nr %s", max_mnode_note_nr) - logging.debug("full_range %s", full_range) - logging.debug("available_nrs %s", available_nrs) - logging.debug('--------------------') - logging.debug("valid_nid_note_ids %s", valid_nid_note_ids) - logging.debug("deleted_note_ids %s", deleted_note_ids) - logging.debug("deleted_nids %s", deleted_nids) - - # add note_id to missing shapes - note_nr_max = max_mnode_note_nr - new_count = 0 - for nr, idx in enumerate(self.mnode_indexes): - mnode_id = mnode_ids[idx] - new_mnode_id = None - mnode = mlayer_node.childNodes[idx] - if mnode_id not in exstg_mnode_note_ids: - if available_nrs: - # use gap in note_id numbering - note_nr = available_nrs.pop(0) - else: - # increment maximum note_id number - note_nr_max = note_nr_max +1 - note_nr = note_nr_max - new_mnode_id = self.occl_id + '-' + str(note_nr) - new_count += 1 - nids[new_mnode_id] = None - else: - # update occlusion type - mnode_id_nr = mnode_id.split('-')[-1] - new_mnode_id = self.occl_id + '-' + mnode_id_nr - nids[new_mnode_id] = nids.pop(mnode_id) - if new_mnode_id: - mnode.setAttribute("id", new_mnode_id) - self.mnode_ids[idx] = new_mnode_id - - logging.debug("=====================") - logging.debug("nr %s", nr) - logging.debug("idx %s", idx) - logging.debug("mnode_id %s", mnode_id) - logging.debug("available_nrs %s", available_nrs) - logging.debug("note_nr_max %s", note_nr_max) - logging.debug("new_mnode_id %s", new_mnode_id) - - logging.debug('--------------------') - logging.debug("edited nids %s", nids) - logging.debug("edited self.mnode_ids %s", self.mnode_ids) - - if del_count or new_count: - q = "This will delete %i card(s) and \ - create %i new one(s).\ - Please note that this action is irreversible.

\ - Would you still like to proceed?" % (del_count, new_count) - if not ioAskUser(q, title="Please confirm action", - parent=mw.ImgOccEdit, help="edit"): - return False - - if deleted_nids: - mw.col.remNotes(deleted_nids) - return (del_count, new_count) - - def _generateMaskSVGsFor(self, side): - """Generate a mask for each mask node""" - masks = [self._createMask(side, node_index) for node_index in self.mnode_indexes] - return masks - - def _createMask(self, side, mask_node_index): - """Call occl_tp-specific mask generator""" - mask_doc = minidom.parseString(self.new_svg.encode('utf-8')) - svg_node = mask_doc.documentElement - layer_nodes = self._layerNodesFrom(svg_node) - mlayer_node = layer_nodes[-1] # treat topmost layer as masks layer - #This method gets implemented differently by subclasses - self._createMaskAtLayernode(side, mask_node_index, mlayer_node) - return svg_node.toxml() - - def _createMaskAtLayernode(self, mask_node_index, mlayer_node): - raise NotImplementedError - - def _setQuestionAttribs(self, node): - """Set question node color and class""" - if (node.nodeType == node.ELEMENT_NODE and node.tagName != "text"): - # set question class - node.setAttribute("class", "qshape") - if node.hasAttribute("fill"): - # set question color - node.setAttribute("fill", self.qfill) - map(self._setQuestionAttribs, node.childNodes) - - def _removeAttribsRecursively(self, node, attrs): - """Remove provided attributes recursively from node and children""" - if (node.nodeType == node.ELEMENT_NODE): - for i in attrs: - if node.hasAttribute(i): - node.removeAttribute(i) - for i in node.childNodes: - self._removeAttribsRecursively(i, attrs) - - def _saveMask(self, mask, note_id, mtype): - """Write mask to file in media collection""" - logging.debug("!saving %s, %s", note_id, mtype) - # media collection is the working directory: - mask_path = '%s-%s.svg' % (note_id, mtype) - mask_file = open(mask_path, 'w') - mask_file.write(mask.encode('utf8')) - mask_file.close() - return mask_path - - def removeBlanks(self, node): - for x in node.childNodes: - if x.nodeType == node.TEXT_NODE: - if x.nodeValue: - x.nodeValue = x.nodeValue.strip() - elif x.nodeType == node.ELEMENT_NODE: - self.removeBlanks(x) - - def _saveMaskAndReturnNote(self, omask_path, qmask, amask, - img, note_id, nid=None): - """Write actual note for given qmask and amask""" - fields = self.fields - model = self.model - mflds = self.mflds - fields[self.ioflds['im']] = img - if omask_path: - # Occlusions updated - qmask_path = self._saveMask(qmask, note_id, "Q") - amask_path = self._saveMask(amask, note_id, "A") - fields[self.ioflds['qm']] = fname2img(qmask_path) - fields[self.ioflds['am']] = fname2img(amask_path) - fields[self.ioflds['om']] = fname2img(omask_path) - fields[self.ioflds['id']] = note_id - - self.model['did'] = self.did - if nid: - note = mw.col.getNote(nid) - else: - note = Note(mw.col, model) - - # add fields to note - note.tags = self.tags - for i in mflds: - fname = i["name"] - if fname in fields: - # only update fields that have been modified - note[fname] = fields[fname] - - if nid: - note.flush() - logging.debug("!noteflush %s", note) - else: - mw.col.addNote(note) - logging.debug("!notecreate %s", note) - - -# Different generator subclasses for different occlusion types: - -class IoGenHideAllRevealOne(ImgOccNoteGenerator): - """Q: All hidden, A: One revealed ('nonoverlapping')""" - occl_tp = "ao" - def __init__(self, ed, svg, image_path, opref, tags, fields, did): - ImgOccNoteGenerator.__init__(self, ed, svg, image_path, - opref, tags, fields, did) - - def _createMaskAtLayernode(self, side, mask_node_index, mlayer_node): - mask_node = mlayer_node.childNodes[mask_node_index] - if side == "Q": - self._setQuestionAttribs(mask_node) - elif side == "A": - mlayer_node.removeChild(mask_node) - -class IoGenHideOneRevealAll(ImgOccNoteGenerator): - """Q: One hidden, A: All revealed ('overlapping')""" - occl_tp = "oa" - def __init__(self, ed, svg, image_path, opref, tags, fields, did): - ImgOccNoteGenerator.__init__(self, ed, svg, image_path, - opref, tags, fields, did) - - def _createMaskAtLayernode(self, side, mask_node_index, mlayer_node): - for i in reversed(self.mnode_indexes): - mask_node = mlayer_node.childNodes[i] - if i == mask_node_index and side == "Q": - self._setQuestionAttribs(mask_node) - mask_node.setAttribute("class", "qshape") - else: - mlayer_node.removeChild(mask_node) diff --git a/image_occlusion_enhanced/options.py b/image_occlusion_enhanced/options.py deleted file mode 100644 index 143b009e..00000000 --- a/image_occlusion_enhanced/options.py +++ /dev/null @@ -1,377 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -## Based on Image Occlusion 2.0 ## -## Copyright (c) 2012-2015 tmbb ## -## (https://github.com/tmbb) ## -## ## -#################################################### - -""" -Main options dialog -""" - -import logging, sys - -from PyQt4 import QtCore, QtGui -from aqt.qt import * -from aqt.utils import showInfo - -from aqt import mw -from anki.errors import AnkiError - -from config import * - -class GrabKey(QDialog): - """ - Grab the key combination to paste the resized image - - Largely based on ImageResizer by searene - (https://github.com/searene/Anki-Addons) - """ - - def __init__(self, parent): - QDialog.__init__(self, parent=parent) - self.parent = parent - self.key = parent.hotkey - # self.active is used to trace whether there's any key held now - self.active = 0 - self.ctrl = False - self.alt = False - self.shift = False - self.extra = None - self.setupUI() - - def setupUI(self): - mainLayout = QVBoxLayout() - self.setLayout(mainLayout) - - label = QLabel('Please press the new key combination') - mainLayout.addWidget(label) - - self.setWindowTitle('Grab key combination') - - def keyPressEvent(self, evt): - self.active += 1 - if evt.key() >0 and evt.key() < 127: - self.extra = chr(evt.key()) - elif evt.key() == Qt.Key_Control: - self.ctrl = True - elif evt.key() == Qt.Key_Alt: - self.alt = True - elif evt.key() == Qt.Key_Shift: - self.shift = True - - def keyReleaseEvent(self, evt): - self.active -= 1 - - if self.active != 0: - return - if not (self.shift or self.ctrl or self.alt): - showInfo("Please use at least one keyboard " - "modifier (Ctrl, Alt, Shift)") - return - if (self.shift and not (self.ctrl or self.alt)): - showInfo("Shift needs to be combined with at " - "least one other modifier (Ctrl, Alt)") - return - if not self.extra: - showInfo("Please press at least one key " - "that is not a keyboard modifier (not Ctrl/Alt/Shift)") - return - - combo = [] - if self.ctrl: - combo.append("Ctrl") - if self.shift: - combo.append("Shift") - if self.alt: - combo.append("Alt") - combo.append(self.extra) - - self.parent.updateHotkey("+".join(combo)) - self.close() - - -class ImgOccOpts(QDialog): - """Main Image Occlusion Options dialog""" - def __init__(self, mw): - QDialog.__init__(self, parent=mw) - loadConfig(self) - self.ofill = self.sconf['ofill'] - self.qfill = self.sconf['qfill'] - self.scol = self.sconf['scol'] - self.swidth = self.sconf['swidth'] - self.font = self.sconf['font'] - self.fsize = self.sconf['fsize'] - self.hotkey = self.lconf["hotkey"] - self.setupUi() - self.setupValues(self.sconf) - - def setupValues(self, config): - """Set up widget data based on provided config dict""" - self.updateHotkey() - self.changeButtonColor(self.ofill_btn, config['ofill']) - self.changeButtonColor(self.qfill_btn, config['qfill']) - self.changeButtonColor(self.scol_btn, config['scol']) - self.swidth_sel.setValue(int(config['swidth'])) - self.fsize_sel.setValue(int(config['fsize'])) - self.swidth_sel.setValue(int(config['swidth'])) - self.font_sel.setCurrentFont(QFont(config['font'])) - self.skipped.setText(','.join(config["skip"])) - - def setupUi(self): - """Set up widgets and layouts""" - - # Top section - qfill_label = QLabel('Question mask') - ofill_label = QLabel('Other masks') - scol_label = QLabel('Lines') - colors_heading = QLabel("Colors") - fields_heading = QLabel("Custom Field Names") - other_heading = QLabel("Other Editor Settings") - - self.qfill_btn = QPushButton() - self.ofill_btn = QPushButton() - self.scol_btn = QPushButton() - self.qfill_btn.connect(self.qfill_btn, SIGNAL("clicked()"), - lambda a="qfill", b=self.qfill_btn: self.getNewColor(a, b)) - self.ofill_btn.connect(self.ofill_btn, SIGNAL("clicked()"), - lambda a="ofill", b=self.ofill_btn: self.getNewColor(a, b)) - self.scol_btn.connect(self.scol_btn, SIGNAL("clicked()"), - lambda a="scol", b=self.scol_btn: self.getNewColor(a, b)) - - swidth_label = QLabel("Line width") - font_label = QLabel("Label font") - fsize_label = QLabel("Label size") - - self.swidth_sel = QSpinBox() - self.swidth_sel.setMinimum(0) - self.swidth_sel.setMaximum(20) - self.font_sel = QFontComboBox() - self.fsize_sel = QSpinBox() - self.fsize_sel.setMinimum(5) - self.fsize_sel.setMaximum(300) - - # Horizontal lines - rule1 = self.create_horizontal_rule() - rule2 = self.create_horizontal_rule() - - # Bottom section and grid assignment - - fields_text = ("Changing any of the entries below will rename " - "the corresponding default field of the IO Enhanced note type. " - "This is the only way you can rename any of the default fields. " - "

Renaming these fields through Anki's regular dialogs " - "will cause the add-on to fail. So please don't do that.") - - fields_description = QLabel(fields_text) - fields_description.setWordWrap(True) - - grid = QtGui.QGridLayout() - grid.setSpacing(10) - - grid.addWidget(colors_heading, 0, 0, 1, 3) - grid.addWidget(qfill_label, 1, 0, 1, 1) - grid.addWidget(self.qfill_btn, 1, 1, 1, 2) - grid.addWidget(ofill_label, 2, 0, 1, 1) - grid.addWidget(self.ofill_btn, 2, 1, 1, 2) - grid.addWidget(scol_label, 3, 0, 1, 1) - grid.addWidget(self.scol_btn, 3, 1, 1, 2) - - grid.addWidget(other_heading, 0, 3, 1, 3) - grid.addWidget(swidth_label, 1, 3, 1, 1) - grid.addWidget(self.swidth_sel, 1, 4, 1, 2) - grid.addWidget(font_label, 2, 3, 1, 1) - grid.addWidget(self.font_sel, 2, 4, 1, 2) - grid.addWidget(fsize_label, 3, 3, 1, 1) - grid.addWidget(self.fsize_sel, 3, 4, 1, 2) - - grid.addWidget(rule1, 4, 0, 1, 6) - grid.addWidget(fields_heading, 5, 0, 1, 6) - grid.addWidget(fields_description, 6, 0, 1, 6) - - # Field name entries - row = 7 - clm = 0 - self.lnedit = {} - for key in IO_FLDS_IDS: - if row == 13: # switch to right columns - clm = 3 - row = 7 - default_name = self.sconf_dflt['flds'][key] - current_name = self.sconf['flds'][key] - l = QLabel(default_name) - l.setTextInteractionFlags(Qt.TextSelectableByMouse) - t = QLineEdit() - t.setText(current_name) - grid.addWidget(l, row, clm, 1, 2) - grid.addWidget(t, row, clm+1, 1, 2) - self.lnedit[key] = t - row = row+1 - - # Misc settings - misc_heading = QLabel("Miscellaneous Settings") - - # Skipped fields: - skipped_description = QLabel("Comma-separated list of " \ - "fields to hide in Editing mode (in order to preserve manual edits):") - self.skipped = QLineEdit() - - # Hotkey: - key_grab_label = QLabel('Invoke IO with the following hotkey:') - self.key_grabbed = QLabel('') - key_grab_btn = QPushButton('Change hotkey', self) - key_grab_btn.clicked.connect(self.showGrabKey) - - grid.addWidget(rule2, row+1, 0, 1, 6) - grid.addWidget(misc_heading, row+2, 0, 1, 6) - grid.addWidget(skipped_description, row+3, 0, 1, 6) - grid.addWidget(self.skipped, row+4, 0, 1, 6) - grid.addWidget(key_grab_label, row+5, 0, 1, 2) - grid.addWidget(self.key_grabbed, row+5, 2, 1, 1) - grid.addWidget(key_grab_btn, row+5, 3, 1, 3) - - # Main button box - button_box = QDialogButtonBox(QDialogButtonBox.Ok - | QDialogButtonBox.Cancel) - defaults_btn = button_box.addButton("Restore &Defaults", - QDialogButtonBox.ResetRole) - self.connect(defaults_btn, SIGNAL("clicked()"), self.restoreDefaults) - button_box.accepted.connect(self.onAccept) - button_box.rejected.connect(self.onReject) - - # Main layout - l_main = QVBoxLayout() - l_main.addLayout(grid) - l_main.addWidget(button_box) - self.setLayout(l_main) - self.setMinimumWidth(800) - self.setMinimumHeight(640) - self.setWindowTitle('Image Occlusion Enhanced Options') - - def create_horizontal_rule(self): - """ - Returns a QFrame that is a sunken, horizontal rule. - """ - frame = QtGui.QFrame() - frame.setFrameShape(QtGui.QFrame.HLine) - frame.setFrameShadow(QtGui.QFrame.Sunken) - return frame - - def updateHotkey(self, combo=None): - """Update hotkey label and attribute""" - key = combo or self.hotkey - label = u"{}".format(key) - self.key_grabbed.setText(label) - if combo: - self.hotkey = combo - - def showGrabKey(self): - """Invoke key grabber""" - win = GrabKey(self) - win.show() - - def getNewColor(self, clrvar, clrbtn): - """Set color via color selection dialog""" - dialog = QColorDialog() - color = dialog.getColor() - if color.isValid(): - # Remove the # sign from QColor.name(): - color = color.name()[1:] - if clrvar == "qfill": - self.qfill = color - elif clrvar == "ofill": - self.ofill = color - elif clrvar == "scol": - self.scol = color - self.changeButtonColor(clrbtn, color) - - def changeButtonColor(self, button, color): - """Generate color preview pixmap and place it on button""" - pixmap = QPixmap(128,18) - qcolour = QtGui.QColor(0, 0, 0) - qcolour.setNamedColor('#' + color) - pixmap.fill(qcolour) - button.setIcon(QIcon(pixmap)) - button.setIconSize(QSize(128, 18)) - - def restoreDefaults(self): - """Restore colors and fields back to defaults""" - self.hotkey = self.lconf_dflt["hotkey"] - for key in self.lnedit.keys(): - self.lnedit[key].setText(IO_FLDS[key]) - self.lnedit[key].setModified(True) - self.setupValues(self.sconf_dflt) - self.ofill = self.sconf_dflt["ofill"] - self.qfill = self.sconf_dflt["qfill"] - self.scol = self.sconf_dflt["scol"] - - def renameFields(self): - """Check for modified names and rename fields accordingly""" - modified = False - model = mw.col.models.byName(IO_MODEL_NAME) - flds = model['flds'] - for key in self.lnedit.keys(): - if not self.lnedit[key].isModified(): - continue - name = self.lnedit[key].text() - oldname = mw.col.conf['imgocc']['flds'][key] - if (name is None or not name.strip() or name == oldname): - continue - fnames = mw.col.models.fieldNames(model) - if (name in fnames and oldname not in fnames): - # case: imported cards, fields not corresponding to config - mw.col.conf['imgocc']['flds'][key] = name - modified = True - continue - idx = fnames.index(oldname) - fld = flds[idx] - if fld: - # rename note type fields - mw.col.models.renameField(model, fld, name) - # update imgocc field-id <-> field-name assignment - mw.col.conf['imgocc']['flds'][key] = name - modified = True - logging.debug("Renamed %s to %s", oldname, name) - if modified: - flds = model['flds'] - - return (modified, flds) - - def onAccept(self): - """Apply changes on OK button press""" - modified = False - try: - (modified, flds) = self.renameFields() - except AnkiError: - print "Field rename action aborted" - return - if modified and hasattr(mw, "ImgOccEdit"): - self.resetIoEditor(flds) - mw.col.conf['imgocc']['ofill'] = self.ofill - mw.col.conf['imgocc']['qfill'] = self.qfill - mw.col.conf['imgocc']['scol'] = self.scol - mw.col.conf['imgocc']['swidth'] = self.swidth_sel.value() - mw.col.conf['imgocc']['fsize'] = self.fsize_sel.value() - mw.col.conf['imgocc']['font'] = self.font_sel.currentFont().family() - mw.col.conf['imgocc']['skip'] = self.skipped.text().split(',') - mw.pm.profile["imgocc"]["hotkey"] = self.hotkey - mw.col.setMod() - self.close() - - def resetIoEditor(self, flds): - """Reset existing instance of IO Editor""" - dialog = mw.ImgOccEdit - loadConfig(dialog) - dialog.resetFields() - dialog.setupFields(flds) - - def onReject(self): - """Dismiss changes on Close button press""" - self.close() diff --git a/image_occlusion_enhanced/resources.py b/image_occlusion_enhanced/resources.py deleted file mode 100644 index 29788b1a..00000000 --- a/image_occlusion_enhanced/resources.py +++ /dev/null @@ -1,272 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created: ter 12. Mar 10:47:51 2013 -# by: The Resource Compiler for PyQt (Qt v4.7.4) -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore - -qt_resource_data = "\ -\x00\x00\x06\xe5\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\ -\x00\x41\x64\x6f\x62\x65\x20\x49\x6d\x61\x67\x65\x52\x65\x61\x64\ -\x79\x71\xc9\x65\x3c\x00\x00\x06\x87\x49\x44\x41\x54\x78\xda\xb4\ -\x57\x6b\x6c\x54\x45\x14\xfe\xe6\x3e\xf6\xee\x16\x4a\xa5\x40\x8b\ -\x85\x10\x20\x96\xa8\x21\x12\x51\x41\x8d\x31\x04\xf1\x81\x28\xe0\ -\x0f\x0c\x51\x13\x31\x1a\xa3\x7f\xc0\x18\x42\xe4\x11\x7f\xf8\x03\ -\x22\x18\x40\x13\x83\x31\x51\xc1\x28\x92\x18\x63\x42\x44\xa2\xf0\ -\x83\x5a\x11\x91\xda\x08\x22\x09\x14\x04\x5a\x4a\x68\xbb\x5d\xdb\ -\xdd\xed\x3e\xee\xee\xde\xf1\x9c\x99\xd9\xdd\x3e\x20\x14\x52\x6e\ -\x72\xb2\xb3\x73\xcf\xcc\x39\xe7\x9b\xef\x9c\x33\x57\x00\xb0\xb7\ -\x2e\xbf\xfb\xbd\x31\xa3\xac\x75\x81\xa4\x7f\x2c\xb7\xf2\x11\x80\ -\x45\x12\xef\x0b\x36\xbe\xbd\xe7\xd4\xbb\x0e\x4d\x85\x3d\xc7\x5a\ -\xe7\xda\x40\x55\x95\x40\x10\x90\x0f\xb7\xc8\x09\xc1\xc6\x2d\xa0\ -\xb7\x57\x82\x6d\xd2\xd4\x46\x76\x20\xc2\x2f\x73\x79\x20\x93\x01\ -\x26\xd5\x02\x85\x82\x24\x27\xe4\x88\x81\x21\x94\x71\x01\xdb\x16\ -\x68\xef\xd0\xb6\xcc\x13\x61\x07\x2c\x1e\xd9\x8e\x40\x32\x25\x11\ -\x8d\xe6\x51\x3f\x25\x8d\x20\x9f\x1b\x39\x28\xc8\xb8\xe5\xb8\x68\ -\x69\x8d\x90\x0d\x47\xd9\x82\xaf\xf6\xb6\x9c\xa2\x8e\x63\xb3\x27\ -\x01\x62\xd1\x3e\x24\xde\x6f\x05\xb2\xf4\x5f\x6a\xf7\xfb\x53\x43\ -\x18\x19\xf6\x43\x0b\x03\x5e\xe0\x01\xb1\x57\xa7\xc0\xad\xa8\x84\ -\x65\xdb\xa5\xd7\xca\x01\x0e\xd4\x21\x1c\x08\x79\xd2\xcf\x61\x56\ -\x08\xd8\x7c\x52\xbf\xe4\xc5\x8c\x58\xc1\x2c\xb0\xcd\x22\x5b\x0e\ -\x72\x44\x0c\x35\xcc\x0f\x51\x4a\xad\x5f\x33\x1b\x38\x4d\x7b\xdb\ -\x64\x87\xa5\x08\x6e\x09\x01\xdb\xd1\x7b\x48\xda\x39\xcb\x0e\xdb\ -\x5a\x91\x0d\xbd\x1c\xbc\x8e\x71\xf8\x43\xe9\x75\x63\x0e\x76\x59\ -\x9f\x2a\xc7\xec\xc1\x76\xad\x7e\xc6\x8d\x01\x36\xc4\x68\xf3\x9e\ -\x36\x6d\xa6\x90\x76\x86\x2e\x51\xec\x14\x6c\x90\x14\xe8\xf4\x11\ -\x32\x0e\xac\xc8\xbf\x86\x71\xf9\xbf\x29\x8c\xb1\x4a\x78\xcc\x73\ -\xac\x17\xb2\x8c\xf0\xd8\x05\x2a\xba\xd7\x62\x74\xfb\xd3\xe5\xb9\ -\xe2\xaf\xd9\x93\xd7\xb0\x0d\xcb\xba\x8a\x03\xfc\xd2\x31\xe2\x1b\ -\x07\x2c\x92\xea\xcc\xef\x64\x98\xce\x24\x6f\x1b\x09\xa9\x39\xd6\ -\x77\x2d\x2d\xca\xd9\x8e\x0d\x94\x5f\x7b\x11\xf8\x6d\xb0\xdb\x16\ -\xab\x23\x75\x8d\x0e\x3b\xe2\xa3\xbc\x7f\x3f\x0a\xf4\xe3\x00\x31\ -\xb3\x20\x19\x57\x81\x9c\xd4\x8a\xec\x2d\x72\x74\x8a\x16\x89\x30\ -\x2c\x90\x34\xa6\x62\xa1\x36\xb3\x4c\x6e\xb3\xd3\xb1\x3d\xb4\xf6\ -\x36\x7a\x4f\x93\xe9\x33\x08\xe7\xce\xc0\x8a\xcc\x50\x75\x45\xa5\ -\xb9\x84\x4a\x43\x25\x64\x4b\x1a\x12\x94\x11\x30\xe4\x50\x47\x40\ -\x8b\x3c\x1a\x87\xb9\x68\xd8\x73\x68\x22\x45\x92\x33\x92\x52\x73\ -\x61\x13\x79\x98\xa0\x4f\xb7\x6d\x83\x1f\x84\x91\x2b\x90\xf3\xc4\ -\x64\xbf\x10\x42\x36\xba\x1b\x9e\xab\x75\x18\x05\xde\xd3\xb6\xcb\ -\x76\x86\x1e\x81\x79\x51\xcc\x06\x86\x8d\x2b\xd4\xbe\xba\x9d\xe8\ -\x75\x66\x52\x95\xea\x52\xc2\x63\x9e\x8b\x50\xe4\x1e\xe1\xd7\x79\ -\x7c\x19\x12\x97\x3f\xa6\xe2\xe5\x20\x9f\x0f\x48\x24\x8d\x6d\x24\ -\x3b\x76\xa3\xf3\xc4\xf3\x4a\x87\x09\x5d\x30\x99\x36\xd8\x81\x12\ -\x1f\x19\x46\x85\x8a\xa3\x95\x5d\x47\xd7\x01\x66\xfb\x8f\xd3\x76\ -\xe9\x5c\x86\x9e\x73\xa4\xce\x00\xce\x9c\x44\xec\x17\x38\xa1\xf1\ -\x94\x6e\x04\xab\xd0\xf4\xe7\x1f\x19\xb8\x48\x74\x37\xc0\x25\x14\ -\x64\x41\xef\xc9\xec\x57\x32\x84\x03\xd0\xde\x71\x4e\xf1\xb9\x73\ -\x53\x72\x4d\x0a\x96\x52\x5a\x0e\xac\xe9\x45\xd4\x0a\x84\x53\x90\ -\xcb\x40\xb8\x02\xe1\x84\x0e\x2d\x53\x19\x40\xd2\x9c\xa4\x77\xbc\ -\x8f\x2a\x46\x52\xeb\x73\x23\x62\x5b\x72\xc0\x11\x48\x1d\x0d\x0b\ -\x93\x8b\x79\xe3\x9a\xe3\x70\xac\x32\xdb\x8b\x52\x64\x38\x07\x72\ -\xdf\xfc\xd3\xb0\xbd\x69\x48\x65\x7b\x51\xd3\x94\x46\x6d\x53\x06\ -\xe9\x6c\x1c\x4e\x78\x3a\xee\x7f\xec\x0c\xb8\xea\x32\x0f\xd8\xa0\ -\x4b\x04\x74\x8c\x1d\xc8\x21\x08\xe8\x3e\x29\xed\x90\x72\x2b\xe2\ -\x99\x52\x7c\x9d\xee\x26\x42\x1e\x1e\x5d\x74\x10\x56\x05\x70\xf2\ -\xe0\x54\x5a\x2a\xb0\x64\x05\x95\xf2\x34\x05\x52\x50\x09\xa3\x03\ -\x72\x99\x33\x1e\x02\x2a\x02\x36\x89\x44\x30\xb4\x12\x4a\x4a\x43\ -\xe1\x7a\x68\xf9\x77\x35\x3a\x3a\x9b\x94\x03\x42\x88\x61\x34\x78\ -\xda\x34\x1c\x41\x21\xe9\xe2\x4a\x75\x02\x0d\x3b\x3d\x9c\x6f\xf5\ -\x55\x64\xcc\x2b\xce\xee\x3b\xa7\x8d\xc3\x42\xcf\xc3\x1d\x94\x0a\ -\xd2\x2e\x57\x50\xa7\x78\x04\x96\xaa\x50\x9c\xa3\x0e\x12\xc9\x0c\ -\xfc\xac\xa7\xa2\x11\xc3\x68\xb6\xec\x64\x10\x58\x68\x9e\x7c\x09\ -\xd1\x99\x79\x2c\xac\x5f\x86\x37\xe7\xcf\x45\x24\x54\xa1\x34\xd2\ -\x7e\x0a\x27\x5a\x8f\x62\x57\xf2\x5b\xcc\xee\x4b\x60\xb9\xa8\xa7\ -\xe0\xc4\x20\x04\x6c\xdd\x79\x1c\x3a\xdc\x78\x3c\x06\x3f\x93\x53\ -\x0e\x0c\xa7\xdb\xdb\xc4\xdc\xa6\x96\x23\xc8\xcd\xf2\xb0\x76\xd1\ -\x56\xb4\xf5\xfc\x86\x03\x67\xd7\xa3\x23\xd9\xa7\x34\x6a\x47\x8f\ -\xc2\xcc\x89\xcf\x61\xc3\xd2\x8f\xb0\x6d\xff\x3a\x7c\x97\x38\x8b\ -\x89\xc1\xd4\xab\x64\x01\x57\x37\x8a\x26\xd6\x17\x25\x16\xfb\xd7\ -\x8c\x5f\xca\x72\x73\xe6\x48\x92\xa9\xff\x10\xf7\xfb\xb0\x7a\xe9\ -\x26\x34\x5e\xd8\x84\x8e\x44\x1b\xb2\xd4\x02\x77\x2c\xd1\x7a\x2b\ -\x7f\x10\x38\xda\xf6\x15\x6a\x7a\x1a\xb0\xf2\xa9\x0d\xf8\x60\xdf\ -\x7a\xcc\xad\x4c\x0c\x44\x80\xd9\xc9\xc7\x1d\x8d\x02\x75\x5f\xb7\ -\x2b\xf6\x5e\xb5\xb7\x93\x14\xa8\xd8\xc8\x40\x5f\x16\x42\x84\xd3\ -\xa1\xfa\xd3\x78\x66\xe9\x4b\xf8\xeb\xf2\xe7\xe8\x4e\xb5\xa9\x60\ -\x7c\x31\x90\xac\x3c\x17\xa3\x77\xc7\x49\x67\xf1\xbd\x2f\xa0\xf1\ -\xe2\x37\x83\x1d\xb0\xd0\x9b\x08\xd0\xd9\x03\x4c\xa8\xb5\x75\xae\ -\x8b\xc1\x91\x6b\xe3\xcb\x56\x4d\xd6\x17\x04\xe5\x83\x83\xcf\xbe\ -\xff\x07\x2f\x4e\xa8\x44\xc3\xf9\x53\xc8\xb3\x0e\x11\xdc\x0f\xca\ -\xeb\x78\x6c\x07\xba\x0e\x5c\x8a\x9f\xc2\xbc\xe9\x0b\xd0\x3e\x36\ -\x53\x76\x80\x33\x30\xa0\x55\x39\xea\x18\x75\xb5\x06\xf8\x6b\x21\ -\x40\x3c\x39\x44\x08\x05\x84\x00\x73\x24\x12\x38\xb0\x2a\x81\x8e\ -\xf4\x31\xe5\xcf\x96\x27\x86\xe6\xee\x8e\x67\xcb\x73\xef\x1c\x14\ -\xe8\x4c\x1d\x2b\xa5\xb8\xba\xf4\xb4\xf7\xa4\xbf\xf8\xb9\x31\xf2\ -\x8a\x2c\xde\x72\xc4\xb5\xaf\x57\x45\x24\x74\x59\x96\x08\x51\xc8\ -\xee\xc3\x40\x92\xdb\xb0\x75\x7d\xca\xb2\x4e\xd2\x6f\x2d\x35\x21\ -\x36\x35\x8a\x84\x30\x45\x0d\x5f\x03\x6e\xe2\xca\xe9\x2e\xd8\x8a\ -\xfd\xf3\x1f\xe4\xdb\x34\xdd\x7c\x72\xba\xf3\x65\x88\x84\xdb\x9f\ -\xd4\x9e\xbe\xf5\x13\x95\x69\x47\x57\x51\xee\x90\xcc\x87\xc6\xc3\ -\xd4\x63\xd6\xa0\x96\x11\xe0\xc3\xb8\xa4\x6e\x5b\xfd\xba\xe3\x0d\ -\x3c\x55\x41\x1c\xcd\xb1\x18\x66\xd7\x8c\x37\xfd\x21\x18\x78\xeb\ -\xa9\xa0\xb0\x2a\x5c\xd3\x6d\x29\xd3\x3a\xbb\x54\x57\x6f\x66\x30\ -\x1d\x73\xdf\xec\x33\x72\x33\x4f\xf6\xca\x9f\xd8\x7e\xae\x1a\x5f\ -\xde\x3e\x81\x0c\x93\xa1\x7c\x41\x5d\x82\x4b\x0f\x47\xed\x9a\x3e\ -\xc3\x9d\xf2\xdc\x79\xa0\x9d\xd6\x60\x60\xb2\xdc\xf4\xc3\xc7\x56\ -\x37\x6f\x0d\x3e\xac\x99\x8a\xc5\x0f\x3d\xa0\x71\xe4\x8f\x8f\x9c\ -\xb9\x44\xa9\xab\x99\xa3\xaf\xc8\x47\x8e\x11\x02\x17\xb0\xf7\xd0\ -\x66\xac\xa2\x99\xcb\xf6\x08\x38\xc0\x09\x17\x5c\x38\x8c\xe6\xf1\ -\x77\x61\x4a\x57\x1c\x33\xb8\x91\x55\x8d\x21\xd8\xc3\x74\x63\x0a\ -\x69\xa2\x75\xd2\x17\x51\xf3\x71\xa0\xeb\x22\xf6\x35\x6c\xc1\x7a\ -\x9a\xba\xc2\x55\x7a\x24\x10\x28\x7e\x2e\x54\x91\x4c\xaa\x7f\x1c\ -\x8f\x4c\x99\x8b\x37\x42\x63\x70\x4f\xff\xaf\x19\x3f\xae\xda\xc1\ -\x27\x2d\x07\xf0\x2b\xcd\xb4\x93\xf4\xf2\xf1\x8f\x94\x03\x45\x27\ -\x38\xa3\xaa\x49\x88\x8e\x18\x6d\xe6\x60\x78\x96\x24\xa1\x3a\x8b\ -\x98\xe1\x5b\x01\x37\xfa\x95\x35\xcc\xef\x50\xd7\x7c\xf0\x86\xfa\ -\xed\xcf\x58\xf0\xcd\x3c\x0d\xfd\x89\x50\xaa\x4c\xff\x0b\x30\x00\ -\x55\xce\x74\xfd\x22\x80\x6f\xfb\x00\x00\x00\x00\x49\x45\x4e\x44\ -\xae\x42\x60\x82\ -\x00\x00\x06\xf7\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\ -\x00\x41\x64\x6f\x62\x65\x20\x49\x6d\x61\x67\x65\x52\x65\x61\x64\ -\x79\x71\xc9\x65\x3c\x00\x00\x06\x99\x49\x44\x41\x54\x78\xda\xb4\ -\x57\x69\x6c\x55\x45\x18\x3d\x33\x77\x7b\x8f\x52\x16\x03\x14\x50\ -\x22\x21\x11\xd4\x18\x13\x85\x44\x8d\x81\xfe\x21\x6e\xe0\x9e\x18\ -\x5c\x82\xd5\x20\x6e\x18\x0c\xc6\x3f\x1a\x89\x71\x41\xa3\x7f\x04\ -\x94\xb8\x21\x06\x88\x46\x0c\x2a\xb2\x48\xd0\x48\x48\x84\x82\x28\ -\x34\xa6\x62\x69\x81\x1a\xca\x22\xa8\x50\x4a\x5f\xe9\xdb\xee\x78\ -\x66\xe6\xbe\xf7\x6e\xdb\xf7\x6a\x13\xf4\x26\x5f\x66\xe6\x2e\x73\ -\xce\x77\xbe\xf9\xbe\x99\x2b\x00\xb8\x77\x2f\xda\xf0\x55\x2e\x14\ -\x33\xf2\x61\x88\xff\xeb\x72\xa4\x84\x2b\xd5\xc6\xb5\xcf\xcd\xbc\ -\x83\xc3\x5c\xe1\xbe\x4b\x4b\x66\x09\xfe\xe9\xf3\xb7\x20\x9d\x07\ -\x94\xe0\x1d\xd1\xeb\x6b\x75\x1e\xc8\xca\x4e\x17\x38\xc0\xbd\xaf\ -\x6e\x9a\xa1\xf1\x68\x67\xe3\x04\x12\xda\xf3\x41\xec\x6c\x3f\xc9\ -\xf7\x75\xc7\x63\xab\x22\xe0\x72\x86\x7e\xda\xde\xc4\xe9\x94\xa0\ -\xbf\x37\x5c\xc4\xae\x55\x38\xd1\x9b\x80\x50\xd1\xc7\x82\xe0\x53\ -\x86\x45\xee\x57\x71\x5c\x95\x80\xa8\x4e\x40\x0e\x4e\xf2\x99\x0f\ -\x91\xe0\xb7\x49\xb6\x7e\x40\x97\x02\x08\x87\x4c\x3d\xb6\x2e\x5b\ -\xa9\xcd\xa7\xd6\x6c\x85\x6b\xfb\x70\xe0\x64\x43\x6c\x7b\xf1\x5d\ -\xcb\xc7\xe2\xf4\xd0\xd7\x8d\x0f\x64\xc2\x10\x86\x1a\x51\x0d\x39\ -\x9c\x0c\x06\x13\xbc\x2a\x89\x70\x50\x00\x49\x60\x24\x09\x16\x68\ -\x02\xec\xd3\x0c\xb0\x13\x19\x09\x08\x0d\xaa\xc1\x45\x44\x42\x50\ -\xf7\xd4\x39\x33\xac\x74\xf5\x20\xe0\x0a\x4b\x4f\x11\x58\x0d\xa2\ -\xf7\x34\x54\xd1\x5b\x12\xd0\x9e\x23\xa1\x3d\xf7\xa1\x34\xb8\xe7\ -\x41\xb9\xd6\x63\x11\x11\x50\x06\xd4\x12\x10\x06\x55\x40\xa5\x1d\ -\x33\xef\x80\x08\x38\xfa\x45\x5f\x2b\x41\xe6\x09\x97\x92\xf3\x31\ -\xfb\x22\x36\x16\x01\xef\xf9\x1c\x7b\xec\x3b\xec\x3b\x7c\x4e\x53\ -\xf4\x56\x14\x09\xb8\x46\x7e\xb3\x00\x39\xa9\x33\x10\x02\x3a\x3c\ -\xae\xb4\x4b\x44\x83\xa8\xa0\x04\x58\x04\xd6\xa6\xfb\x3e\x5b\x97\ -\xad\xab\x63\x6d\x4d\xc8\x08\x54\xcb\xce\x56\x40\x9a\x78\x2a\xc7\ -\xce\x1b\xc6\x73\xaf\x12\x01\x8d\x2f\x98\x2f\x8a\x1e\x1a\xe0\x20\ -\xf2\xbc\x00\x9c\x88\xee\x91\x80\xf0\xac\xf7\x45\xf0\x08\xd8\x4e\ -\xe9\x58\x15\x32\x39\x23\x2b\x4b\x80\x01\x4f\x89\xbe\xa9\x22\xfb\ -\xac\x81\x20\x06\xde\xa3\x75\x4d\x32\x8b\xa0\xa4\x86\x7d\xd7\xb1\ -\x21\x29\x98\x17\xb5\x9c\x59\x08\x41\xa2\x02\x2e\x87\x59\xce\xff\ -\x3e\x02\x34\x2e\xda\x7c\xc2\x06\xba\x17\x81\xb0\xb8\x06\x6c\x8c\ -\x0b\xb1\xd6\x72\x5b\x12\x4e\x5f\x25\xcc\x3b\x3a\x24\xfa\x99\x17\ -\xbd\x1b\xb5\x9e\x25\x23\xa9\x52\xc0\xb0\x1e\x5a\xb8\x05\x43\xae\ -\xb8\x1c\x72\xfc\x04\xfc\xf4\xfc\xe7\xe9\x02\x89\x3e\x0a\xe8\x8f\ -\x84\xef\xf5\x04\xf7\xdd\xa2\x2a\x45\x8f\x13\x9e\x25\x62\xd2\x93\ -\xc5\xcd\xd4\x88\x2a\x66\x4e\x64\xbc\x27\x49\xc6\x67\xb6\xfc\xf0\ -\xc9\xa7\xf8\x7e\xdc\x21\x74\x9f\x3d\x47\xeb\x42\xd7\x88\x71\xf8\ -\xf1\xe9\x15\xe9\xc8\x5d\x14\xab\x43\x61\x0d\xc0\x8b\x49\x59\xf0\ -\xd0\xb7\x2a\x40\x7b\x18\xe8\x3a\xc0\x94\x74\x59\x17\xba\x52\xc8\ -\x9f\x3e\x0c\xd5\x7e\x0a\xaa\xa3\x03\xf9\x3f\xa8\x70\x7b\x0a\xe8\ -\xc8\xc0\xf9\xbb\x03\xcb\x17\x7e\x89\xa9\x57\x4e\xc2\x05\xc3\xaa\ -\xb1\x7c\xe5\x0a\xdc\x24\xa7\xa3\xbb\x3b\x0d\x3f\x9b\x36\xe5\xce\ -\xed\xbd\x22\x15\xbd\xd7\xf2\x19\x8b\xc7\xd4\x64\x03\xf3\x3f\xa9\ -\xd3\x44\x41\x9d\xda\x8f\xf0\xcc\x71\x28\x12\xd0\x01\x56\x69\xde\ -\xeb\x56\x26\xe6\x48\xb0\x72\x76\x01\x8d\xf7\x5c\x86\x21\x41\x15\ -\x86\x0e\x19\x86\x03\xcd\xfb\x51\x73\xc1\x48\x7c\x76\x64\x1d\xee\ -\x3f\x79\x31\x26\x2f\x7b\xec\xda\x1e\x59\x20\x0a\x75\xd9\x2d\x29\ -\xa0\xfb\xc6\x7b\xcf\xb7\x5e\x8b\x2c\xc2\x13\x0d\x50\xd9\x76\x2e\ -\x18\xd7\x7c\xa0\xcb\xb5\xca\x84\xcc\x04\xae\x70\xc5\xbc\xcb\xe9\ -\x95\x9e\x45\xc3\xd4\x0b\x31\xad\x6e\x26\xae\x1b\x3d\x16\x6f\xaf\ -\xde\x06\x37\xdb\x89\xa6\xa6\xdf\x10\x52\xc5\xc9\xcb\xe6\x4e\xe3\ -\x4b\x47\x75\x62\xc8\x78\x1a\x3a\x86\x92\x9e\xd8\x82\x9b\x08\x79\ -\x9e\x21\x13\x9e\x69\x43\x78\x6c\x0f\xeb\x79\x8a\xa0\x94\x7e\x10\ -\x95\x4a\x46\x99\x61\x94\xe2\x8a\x77\xa8\x40\x3e\x8f\x86\xcb\x46\ -\x60\xda\x43\xb7\x01\x23\xeb\xb8\xc3\xbd\x86\x79\xb9\xd9\x68\x69\ -\x3b\xc1\xda\xe5\x62\xe9\xeb\x6f\x6a\xf0\x56\x1a\xb7\x3e\x64\x64\ -\x01\xbc\xb8\x06\x58\x35\xb4\xe9\x4a\x07\x5d\xeb\x43\xed\xf5\x3e\ -\xa8\x14\x63\xab\x43\x60\x6a\x03\x41\x93\xd2\x54\x48\xe1\xeb\x77\ -\xa5\xad\x78\xdc\x6d\xf6\x8c\x1e\x8a\x69\x0f\x13\x7c\xd4\x83\xc0\ -\x8e\x97\x81\xa6\x0d\x9c\xc3\xc1\xd5\x27\x3f\x40\x53\xcd\xed\x88\ -\x3c\x37\xe0\x3d\xb2\xa0\x10\x02\x53\x5c\x8c\x02\x1c\xe5\x33\x08\ -\xff\x68\xe6\x04\x19\x5b\x78\x34\xb1\x80\x60\x34\x24\x4a\x7d\x53\ -\x91\x59\xf6\x76\xfb\x49\xd4\xce\x21\x48\x0d\xc1\xeb\x5f\x02\x9a\ -\xbf\x36\xfb\xf1\x3b\xf5\x63\x71\xff\x7b\x07\x18\xaa\x94\x46\xe8\ -\x2c\x80\xf7\x21\xa0\x43\xa0\x22\xf9\x55\x5e\x7b\x7e\xd0\xbe\xe1\ -\xc8\xa8\xca\x52\x66\xe6\xaa\xf6\x5a\x24\xf5\x1e\x21\x4d\x25\x16\ -\x22\xc4\x8f\x9d\x2e\x6a\x1f\x89\x3c\x37\xe0\xeb\x8d\x32\x4b\xf6\ -\x8c\xc7\x03\x1f\xef\xb7\x61\xce\x65\xfe\xa5\x12\x46\x0a\xe8\x7d\ -\x3b\x3c\xde\x6a\x83\x23\x4d\x49\xb3\x66\x62\x24\x6c\x79\x65\x28\ -\xa4\x26\x43\xf0\x5d\x6d\x21\x6a\xe7\x6a\xcf\xeb\x62\xe0\x2e\x96\ -\x34\x5e\x8a\x87\x76\xfc\x5a\x3c\x00\x28\xf5\x2f\xa5\xd8\xcc\x4f\ -\xc0\xf0\x28\xc1\xa9\x00\xf4\x09\x86\x8b\x0a\x21\x2b\x79\x98\x2f\ -\x99\x0a\xcd\x8a\x17\xb4\x9d\xbf\x64\x50\xfb\x28\x8f\x79\xa3\xeb\ -\x4a\xb2\x93\xe0\x92\x5f\x2e\xc1\xc3\xfb\x5a\x90\x97\x79\xf4\xb3\ -\x19\xf6\xda\x8e\x69\xe9\xb6\x56\x7d\x48\x23\x70\xa0\xe3\x01\x11\ -\x7a\xa5\x5d\x4e\x44\x27\x1f\x97\x67\x05\x11\xa0\xbe\x3e\x8b\xda\ -\xc7\xef\x8c\x81\xaf\x33\x31\x5f\xbc\x6b\x3c\xe6\x1c\xd9\x8b\x5c\ -\x67\x06\xf9\xae\xb0\x54\xf8\x2b\x11\x28\x28\x63\xea\x40\xc3\xb9\ -\x7e\xcf\x98\x85\x6c\xa9\x5f\x30\x09\xb5\xf3\x66\x01\x63\x62\xe0\ -\x2a\x87\xc5\x3b\x46\xe1\xc9\x55\x7b\xcd\xc9\x4a\xda\xe3\xa5\x4d\ -\xef\x18\x4e\xc5\x10\x98\x0d\x55\x45\x6d\x7c\x1c\x99\x1b\xed\x20\ -\xbb\xe6\xd5\xa0\xf6\x89\xbb\xfa\x82\x6f\x1f\x89\xf9\xab\x9a\x7b\ -\x10\x2d\xcc\xd7\xaf\x02\x42\x94\x06\x52\x94\x71\x39\xd6\x6d\xae\ -\xff\x02\x35\x33\x1e\x00\xc6\x2f\x00\xb6\x3d\x03\xb4\x6c\x34\xb2\ -\x2f\xad\x1f\x85\xf9\xab\x9b\x8b\x5e\xc5\xd3\xab\xd0\x17\xa2\x82\ -\x02\xae\x94\x3d\x6e\x54\x32\xfd\xfd\x5f\xcd\x5b\x31\xe9\xc6\x17\ -\x90\x6b\xdb\xca\x32\xd1\xcd\x07\x39\x7c\xf4\xf3\x58\x3c\xb5\xb2\ -\xb9\xe2\x1c\x45\x6f\xa5\x2c\xaf\x80\xe7\xa8\xb3\xb7\xbe\xb2\xa9\ -\x5a\x9f\xdb\xcb\xc5\x89\x05\x16\x79\x2e\xc0\x89\xb9\x5d\x78\x75\ -\x3a\x33\xe0\xd8\x37\x10\xa7\x5b\xb0\xaf\xf5\x2c\x1a\x7f\xbd\x08\ -\x6b\x26\x7e\x8c\xb5\x8b\xbe\x63\xb2\xa4\xcb\x06\xda\x64\x2e\xc1\ -\x7d\xe2\x94\x5b\x53\xc3\x68\x13\xae\xb8\x61\xf6\xf5\xb9\x6c\x5a\ -\x95\x41\x57\xf9\x7c\x36\x97\x6a\xff\x3b\x53\x77\x8d\xff\x61\x30\ -\x7c\x8c\x9c\x77\xf3\x70\xac\xff\xf2\xdb\xf4\x96\x9d\xbf\xbf\xb5\ -\xb3\xea\xee\xc3\x9e\xcc\x0b\xa1\x42\xd1\xdf\x2f\x94\xeb\x05\xa2\ -\x71\xcb\xca\xed\xec\x1e\xa2\xb5\xc7\x09\xe8\x85\x5a\x6d\x7f\x45\ -\x20\x2b\x2d\xfe\x67\xef\x9b\x32\xab\xe9\xdc\xc4\xd7\xc6\x79\x6d\ -\x59\xd1\x71\xf0\xad\x77\x36\x1f\xd3\x75\xf6\x14\xad\x6b\x80\x3f\ -\x6f\xfa\xd0\x95\x8a\xfe\x8a\xb2\xf1\x10\xe8\xc1\x69\xda\x99\x0a\ -\x04\xf4\xe4\x49\x39\xf2\xaa\x67\x6b\xda\x1a\xde\x58\xb6\x66\x37\ -\x77\x17\xfc\x19\x81\x77\x46\xdf\xab\x32\x7f\x94\xe5\x08\x84\xbd\ -\xc9\x8a\x01\xfe\x62\x0e\xa6\x8d\x8a\x32\xb3\x23\xb2\x74\x34\xe1\ -\x79\x5d\x03\x25\xa0\xc3\x14\xd8\x5f\xcd\xff\x06\xb8\x70\xfd\x23\ -\xc0\x00\x10\xd7\x1c\xa6\xef\x42\x19\x7e\x00\x00\x00\x00\x49\x45\ -\x4e\x44\xae\x42\x60\x82\ -" - -qt_resource_name = "\ -\x00\x05\ -\x00\x6f\xa6\x53\ -\x00\x69\ -\x00\x63\x00\x6f\x00\x6e\x00\x73\ -\x00\x11\ -\x06\xc6\x1b\xe7\ -\x00\x6e\ -\x00\x65\x00\x77\x00\x5f\x00\x6f\x00\x63\x00\x63\x00\x6c\x00\x75\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\ -\x00\x12\ -\x01\xc1\x07\xe7\ -\x00\x65\ -\x00\x64\x00\x69\x00\x74\x00\x5f\x00\x6f\x00\x63\x00\x63\x00\x6c\x00\x75\x00\x73\x00\x69\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\ -\x00\x67\ -" - -qt_resource_struct = "\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ -\x00\x00\x00\x38\x00\x00\x00\x00\x00\x01\x00\x00\x06\xe9\ -\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -" - -def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() diff --git a/image_occlusion_enhanced/resources.qrc b/image_occlusion_enhanced/resources.qrc deleted file mode 100644 index fe01eef3..00000000 --- a/image_occlusion_enhanced/resources.qrc +++ /dev/null @@ -1,7 +0,0 @@ - - - - icons/new_occlusion.png - icons/edit_occlusion.png - - \ No newline at end of file diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/browser-not-supported.html b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/browser-not-supported.html deleted file mode 100644 index 3010fcf3..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/browser-not-supported.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - -Browser does not support SVG | SVG-edit - - - -
-SVG-edit logo
-

Sorry, but your browser does not support SVG. Below is a list of alternate browsers and versions that support SVG and SVG-edit (from caniuse.com).

-

Try the latest version of Firefox, Google Chrome, Safari, Opera or Internet Explorer.

-

If you are unable to install one of these and must use an old version of Internet Explorer, you can install the Google Chrome Frame plugin.

- - - -
- - - diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/browser.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/browser.js deleted file mode 100644 index 7684d0e7..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/browser.js +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Package: svgedit.browser - * - * Licensed under the MIT License - * - * Copyright(c) 2010 Jeff Schiller - * Copyright(c) 2010 Alexis Deveria - * Copyright(c) 2016 Glutanimate - */ - -// Dependencies: -// 1) jQuery (for $.alert()) - -// Modifications by Glutanimate: -// - disabled touch detection (not working properly) - -var svgedit = svgedit || {}; - -(function() { - -if (!svgedit.browser) { - svgedit.browser = {}; -} -var supportsSvg_ = (function() { - return !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect; -})(); -svgedit.browser.supportsSvg = function() { return supportsSvg_; } -if(!svgedit.browser.supportsSvg()) { - window.location = "browser-not-supported.html"; -} -else{ - -var svgns = 'http://www.w3.org/2000/svg'; -var userAgent = navigator.userAgent; -var svg = document.createElementNS(svgns, 'svg'); - -// Note: Browser sniffing should only be used if no other detection method is possible -var isOpera_ = !!window.opera; -var isWebkit_ = userAgent.indexOf("AppleWebKit") >= 0; -var isGecko_ = userAgent.indexOf('Gecko/') >= 0; -var isIE_ = userAgent.indexOf('MSIE') >= 0; -var isChrome_ = userAgent.indexOf('Chrome/') >= 0; -var isWindows_ = userAgent.indexOf('Windows') >= 0; -var isMac_ = userAgent.indexOf('Macintosh') >= 0; -var isTouch_ = false; - -var supportsSelectors_ = (function() { - return !!svg.querySelector; -})(); - -var supportsXpath_ = (function() { - return !!document.evaluate; -})(); - -// segList functions (for FF1.5 and 2.0) -var supportsPathReplaceItem_ = (function() { - var path = document.createElementNS(svgns, 'path'); - path.setAttribute('d','M0,0 10,10'); - var seglist = path.pathSegList; - var seg = path.createSVGPathSegLinetoAbs(5,5); - try { - seglist.replaceItem(seg, 0); - return true; - } catch(err) {} - return false; -})(); - -var supportsPathInsertItemBefore_ = (function() { - var path = document.createElementNS(svgns,'path'); - path.setAttribute('d','M0,0 10,10'); - var seglist = path.pathSegList; - var seg = path.createSVGPathSegLinetoAbs(5,5); - try { - seglist.insertItemBefore(seg, 0); - return true; - } catch(err) {} - return false; -})(); - -// text character positioning (for IE9) -var supportsGoodTextCharPos_ = (function() { - var retValue = false; - var svgroot = document.createElementNS(svgns, 'svg'); - var svgcontent = document.createElementNS(svgns, 'svg'); - document.documentElement.appendChild(svgroot); - svgcontent.setAttribute('x', 5); - svgroot.appendChild(svgcontent); - var text = document.createElementNS(svgns,'text'); - text.textContent = 'a'; - svgcontent.appendChild(text); - var pos = text.getStartPositionOfChar(0).x; - document.documentElement.removeChild(svgroot); - return (pos === 0); -})(); - -var supportsPathBBox_ = (function() { - var svgcontent = document.createElementNS(svgns, 'svg'); - document.documentElement.appendChild(svgcontent); - var path = document.createElementNS(svgns, 'path'); - path.setAttribute('d','M0,0 C0,0 10,10 10,0'); - svgcontent.appendChild(path); - var bbox = path.getBBox(); - document.documentElement.removeChild(svgcontent); - return (bbox.height > 4 && bbox.height < 5); -})(); - -// Support for correct bbox sizing on groups with horizontal/vertical lines -var supportsHVLineContainerBBox_ = (function() { - var svgcontent = document.createElementNS(svgns, 'svg'); - document.documentElement.appendChild(svgcontent); - var path = document.createElementNS(svgns, 'path'); - path.setAttribute('d','M0,0 10,0'); - var path2 = document.createElementNS(svgns, 'path'); - path2.setAttribute('d','M5,0 15,0'); - var g = document.createElementNS(svgns, 'g'); - g.appendChild(path); - g.appendChild(path2); - svgcontent.appendChild(g); - var bbox = g.getBBox(); - document.documentElement.removeChild(svgcontent); - // Webkit gives 0, FF gives 10, Opera (correctly) gives 15 - return (bbox.width == 15); -})(); - -var supportsEditableText_ = (function() { - // TODO: Find better way to check support for this - return isOpera_; -})(); - -var supportsGoodDecimals_ = (function() { - // Correct decimals on clone attributes (Opera < 10.5/win/non-en) - var rect = document.createElementNS(svgns, 'rect'); - rect.setAttribute('x',.1); - var crect = rect.cloneNode(false); - var retValue = (crect.getAttribute('x').indexOf(',') == -1); - if(!retValue) { - $.alert("NOTE: This version of Opera is known to contain bugs in SVG-edit.\n\ - Please upgrade to the latest version in which the problems have been fixed."); - } - return retValue; -})(); - -var supportsNonScalingStroke_ = (function() { - var rect = document.createElementNS(svgns, 'rect'); - rect.setAttribute('style','vector-effect:non-scaling-stroke'); - return rect.style.vectorEffect === 'non-scaling-stroke'; -})(); - -var supportsNativeSVGTransformLists_ = (function() { - var rect = document.createElementNS(svgns, 'rect'); - var rxform = rect.transform.baseVal; - - var t1 = svg.createSVGTransform(); - rxform.appendItem(t1); - return rxform.getItem(0) == t1; -})(); - -// Public API - -svgedit.browser.isOpera = function() { return isOpera_; } -svgedit.browser.isWebkit = function() { return isWebkit_; } -svgedit.browser.isGecko = function() { return isGecko_; } -svgedit.browser.isIE = function() { return isIE_; } -svgedit.browser.isChrome = function() { return isChrome_; } -svgedit.browser.isWindows = function() { return isWindows_; } -svgedit.browser.isMac = function() { return isMac_; } -svgedit.browser.isTouch = function() { return isTouch_; } - -svgedit.browser.supportsSelectors = function() { return supportsSelectors_; } -svgedit.browser.supportsXpath = function() { return supportsXpath_; } - -svgedit.browser.supportsPathReplaceItem = function() { return supportsPathReplaceItem_; } -svgedit.browser.supportsPathInsertItemBefore = function() { return supportsPathInsertItemBefore_; } -svgedit.browser.supportsPathBBox = function() { return supportsPathBBox_; } -svgedit.browser.supportsHVLineContainerBBox = function() { return supportsHVLineContainerBBox_; } -svgedit.browser.supportsGoodTextCharPos = function() { return supportsGoodTextCharPos_; } -svgedit.browser.supportsEditableText = function() { return supportsEditableText_; } -svgedit.browser.supportsGoodDecimals = function() { return supportsGoodDecimals_; } -svgedit.browser.supportsNonScalingStroke = function() { return supportsNonScalingStroke_; } -svgedit.browser.supportsNativeTransformLists = function() { return supportsNativeSVGTransformLists_; } - -} - -})(); diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/canvg/canvg.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/canvg/canvg.js deleted file mode 100644 index 7b24a383..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/canvg/canvg.js +++ /dev/null @@ -1,2620 +0,0 @@ -/* - * canvg.js - Javascript SVG parser and renderer on Canvas - * MIT Licensed - * Gabe Lerner (gabelerner@gmail.com) - * http://code.google.com/p/canvg/ - * - * Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/ - */ -if(!window.console) { - window.console = {}; - window.console.log = function(str) {}; - window.console.dir = function(str) {}; -} - -if(!Array.prototype.indexOf){ - Array.prototype.indexOf = function(obj){ - for(var i=0; i ignore mouse events - // ignoreAnimation: true => ignore animations - // ignoreDimensions: true => does not try to resize canvas - // ignoreClear: true => does not clear canvas - // offsetX: int => draws at a x offset - // offsetY: int => draws at a y offset - // scaleWidth: int => scales horizontally to width - // scaleHeight: int => scales vertically to height - // renderCallback: function => will call the function after the first render is completed - // forceRedraw: function => will call the function on every frame, if it returns true, will redraw - this.canvg = function (target, s, opts) { - // no parameters - if (target == null && s == null && opts == null) { - var svgTags = document.getElementsByTagName('svg'); - for (var i=0; i]*>/, ''); - var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); - xmlDoc.async = 'false'; - xmlDoc.loadXML(xml); - return xmlDoc; - } - } - - svg.Property = function(name, value) { - this.name = name; - this.value = value; - - this.hasValue = function() { - return (this.value != null && this.value !== ''); - } - - // return the numerical value of the property - this.numValue = function() { - if (!this.hasValue()) return 0; - - var n = parseFloat(this.value); - if ((this.value + '').match(/%$/)) { - n = n / 100.0; - } - return n; - } - - this.valueOrDefault = function(def) { - if (this.hasValue()) return this.value; - return def; - } - - this.numValueOrDefault = function(def) { - if (this.hasValue()) return this.numValue(); - return def; - } - - /* EXTENSIONS */ - var that = this; - - // color extensions - this.Color = { - // augment the current color value with the opacity - addOpacity: function(opacity) { - var newValue = that.value; - if (opacity != null && opacity != '') { - var color = new RGBColor(that.value); - if (color.ok) { - newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacity + ')'; - } - } - return new svg.Property(that.name, newValue); - } - } - - // definition extensions - this.Definition = { - // get the definition from the definitions table - getDefinition: function() { - var name = that.value.replace(/^(url\()?#([^\)]+)\)?$/, '$2'); - return svg.Definitions[name]; - }, - - isUrl: function() { - return that.value.indexOf('url(') == 0 - }, - - getFillStyle: function(e) { - var def = this.getDefinition(); - - // gradient - if (def != null && def.createGradient) { - return def.createGradient(svg.ctx, e); - } - - // pattern - if (def != null && def.createPattern) { - return def.createPattern(svg.ctx, e); - } - - return null; - } - } - - // length extensions - this.Length = { - DPI: function(viewPort) { - return 96.0; // TODO: compute? - }, - - EM: function(viewPort) { - var em = 12; - - var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); - if (fontSize.hasValue()) em = fontSize.Length.toPixels(viewPort); - - return em; - }, - - // get the length as pixels - toPixels: function(viewPort) { - if (!that.hasValue()) return 0; - var s = that.value+''; - if (s.match(/em$/)) return that.numValue() * this.EM(viewPort); - if (s.match(/ex$/)) return that.numValue() * this.EM(viewPort) / 2.0; - if (s.match(/px$/)) return that.numValue(); - if (s.match(/pt$/)) return that.numValue() * 1.25; - if (s.match(/pc$/)) return that.numValue() * 15; - if (s.match(/cm$/)) return that.numValue() * this.DPI(viewPort) / 2.54; - if (s.match(/mm$/)) return that.numValue() * this.DPI(viewPort) / 25.4; - if (s.match(/in$/)) return that.numValue() * this.DPI(viewPort); - if (s.match(/%$/)) return that.numValue() * svg.ViewPort.ComputeSize(viewPort); - return that.numValue(); - } - } - - // time extensions - this.Time = { - // get the time as milliseconds - toMilliseconds: function() { - if (!that.hasValue()) return 0; - var s = that.value+''; - if (s.match(/s$/)) return that.numValue() * 1000; - if (s.match(/ms$/)) return that.numValue(); - return that.numValue(); - } - } - - // angle extensions - this.Angle = { - // get the angle as radians - toRadians: function() { - if (!that.hasValue()) return 0; - var s = that.value+''; - if (s.match(/deg$/)) return that.numValue() * (Math.PI / 180.0); - if (s.match(/grad$/)) return that.numValue() * (Math.PI / 200.0); - if (s.match(/rad$/)) return that.numValue(); - return that.numValue() * (Math.PI / 180.0); - } - } - } - - // fonts - svg.Font = new (function() { - this.Styles = ['normal','italic','oblique','inherit']; - this.Variants = ['normal','small-caps','inherit']; - this.Weights = ['normal','bold','bolder','lighter','100','200','300','400','500','600','700','800','900','inherit']; - - this.CreateFont = function(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { - var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); - return { - fontFamily: fontFamily || f.fontFamily, - fontSize: fontSize || f.fontSize, - fontStyle: fontStyle || f.fontStyle, - fontWeight: fontWeight || f.fontWeight, - fontVariant: fontVariant || f.fontVariant, - toString: function () { return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' ') } - } - } - - var that = this; - this.Parse = function(s) { - var f = {}; - var d = svg.trim(svg.compressSpaces(s || '')).split(' '); - var set = { fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false } - var ff = ''; - for (var i=0; i this.x2) this.x2 = x; - } - - if (y != null) { - if (isNaN(this.y1) || isNaN(this.y2)) { - this.y1 = y; - this.y2 = y; - } - if (y < this.y1) this.y1 = y; - if (y > this.y2) this.y2 = y; - } - } - this.addX = function(x) { this.addPoint(x, null); } - this.addY = function(y) { this.addPoint(null, y); } - - this.addBoundingBox = function(bb) { - this.addPoint(bb.x1, bb.y1); - this.addPoint(bb.x2, bb.y2); - } - - this.addQuadraticCurve = function(p0x, p0y, p1x, p1y, p2x, p2y) { - var cp1x = p0x + 2/3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) - var cp1y = p0y + 2/3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) - var cp2x = cp1x + 1/3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) - var cp2y = cp1y + 1/3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) - this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); - } - - this.addBezierCurve = function(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { - // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html - var p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; - this.addPoint(p0[0], p0[1]); - this.addPoint(p3[0], p3[1]); - - for (i=0; i<=1; i++) { - var f = function(t) { - return Math.pow(1-t, 3) * p0[i] - + 3 * Math.pow(1-t, 2) * t * p1[i] - + 3 * (1-t) * Math.pow(t, 2) * p2[i] - + Math.pow(t, 3) * p3[i]; - } - - var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; - var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; - var c = 3 * p1[i] - 3 * p0[i]; - - if (a == 0) { - if (b == 0) continue; - var t = -c / b; - if (0 < t && t < 1) { - if (i == 0) this.addX(f(t)); - if (i == 1) this.addY(f(t)); - } - continue; - } - - var b2ac = Math.pow(b, 2) - 4 * c * a; - if (b2ac < 0) continue; - var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); - if (0 < t1 && t1 < 1) { - if (i == 0) this.addX(f(t1)); - if (i == 1) this.addY(f(t1)); - } - var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); - if (0 < t2 && t2 < 1) { - if (i == 0) this.addX(f(t2)); - if (i == 1) this.addY(f(t2)); - } - } - } - - this.isPointInBox = function(x, y) { - return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); - } - - this.addPoint(x1, y1); - this.addPoint(x2, y2); - } - - // transforms - svg.Transform = function(v) { - var that = this; - this.Type = {} - - // translate - this.Type.translate = function(s) { - this.p = svg.CreatePoint(s); - this.apply = function(ctx) { - ctx.translate(this.p.x || 0.0, this.p.y || 0.0); - } - this.applyToPoint = function(p) { - p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); - } - } - - // rotate - this.Type.rotate = function(s) { - var a = svg.ToNumberArray(s); - this.angle = new svg.Property('angle', a[0]); - this.cx = a[1] || 0; - this.cy = a[2] || 0; - this.apply = function(ctx) { - ctx.translate(this.cx, this.cy); - ctx.rotate(this.angle.Angle.toRadians()); - ctx.translate(-this.cx, -this.cy); - } - this.applyToPoint = function(p) { - var a = this.angle.Angle.toRadians(); - p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); - p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); - p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); - } - } - - this.Type.scale = function(s) { - this.p = svg.CreatePoint(s); - this.apply = function(ctx) { - ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); - } - this.applyToPoint = function(p) { - p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); - } - } - - this.Type.matrix = function(s) { - this.m = svg.ToNumberArray(s); - this.apply = function(ctx) { - ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); - } - this.applyToPoint = function(p) { - p.applyTransform(this.m); - } - } - - this.Type.SkewBase = function(s) { - this.base = that.Type.matrix; - this.base(s); - this.angle = new svg.Property('angle', s); - } - this.Type.SkewBase.prototype = new this.Type.matrix; - - this.Type.skewX = function(s) { - this.base = that.Type.SkewBase; - this.base(s); - this.m = [1, 0, Math.tan(this.angle.Angle.toRadians()), 1, 0, 0]; - } - this.Type.skewX.prototype = new this.Type.SkewBase; - - this.Type.skewY = function(s) { - this.base = that.Type.SkewBase; - this.base(s); - this.m = [1, Math.tan(this.angle.Angle.toRadians()), 0, 1, 0, 0]; - } - this.Type.skewY.prototype = new this.Type.SkewBase; - - this.transforms = []; - - this.apply = function(ctx) { - for (var i=0; i= this.tokens.length - 1; - } - - this.isCommandOrEnd = function() { - if (this.isEnd()) return true; - return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; - } - - this.isRelativeCommand = function() { - return this.command == this.command.toLowerCase(); - } - - this.getToken = function() { - this.i = this.i + 1; - return this.tokens[this.i]; - } - - this.getScalar = function() { - return parseFloat(this.getToken()); - } - - this.nextCommand = function() { - this.previousCommand = this.command; - this.command = this.getToken(); - } - - this.getPoint = function() { - var p = new svg.Point(this.getScalar(), this.getScalar()); - return this.makeAbsolute(p); - } - - this.getAsControlPoint = function() { - var p = this.getPoint(); - this.control = p; - return p; - } - - this.getAsCurrentPoint = function() { - var p = this.getPoint(); - this.current = p; - return p; - } - - this.getReflectedControlPoint = function() { - if (this.previousCommand.toLowerCase() != 'c' && this.previousCommand.toLowerCase() != 's') { - return this.current; - } - - // reflect point - var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); - return p; - } - - this.makeAbsolute = function(p) { - if (this.isRelativeCommand()) { - p.x = this.current.x + p.x; - p.y = this.current.y + p.y; - } - return p; - } - - this.addMarker = function(p, from, priorTo) { - // if the last angle isn't filled in because we didn't have this point yet ... - if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length-1] == null) { - this.angles[this.angles.length-1] = this.points[this.points.length-1].angleTo(priorTo); - } - this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); - } - - this.addMarkerAngle = function(p, a) { - this.points.push(p); - this.angles.push(a); - } - - this.getMarkerPoints = function() { return this.points; } - this.getMarkerAngles = function() { - for (var i=0; i 1) { - rx *= Math.sqrt(l); - ry *= Math.sqrt(l); - } - // cx', cy' - var s = (largeArcFlag == sweepFlag ? -1 : 1) * Math.sqrt( - ((Math.pow(rx,2)*Math.pow(ry,2))-(Math.pow(rx,2)*Math.pow(currp.y,2))-(Math.pow(ry,2)*Math.pow(currp.x,2))) / - (Math.pow(rx,2)*Math.pow(currp.y,2)+Math.pow(ry,2)*Math.pow(currp.x,2)) - ); - if (isNaN(s)) s = 0; - var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); - // cx, cy - var centp = new svg.Point( - (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, - (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y - ); - // vector magnitude - var m = function(v) { return Math.sqrt(Math.pow(v[0],2) + Math.pow(v[1],2)); } - // ratio between two vectors - var r = function(u, v) { return (u[0]*v[0]+u[1]*v[1]) / (m(u)*m(v)) } - // angle between two vectors - var a = function(u, v) { return (u[0]*v[1] < u[1]*v[0] ? -1 : 1) * Math.acos(r(u,v)); } - // initial angle - var a1 = a([1,0], [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]); - // angle delta - var u = [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]; - var v = [(-currp.x-cpp.x)/rx,(-currp.y-cpp.y)/ry]; - var ad = a(u, v); - if (r(u,v) <= -1) ad = Math.PI; - if (r(u,v) >= 1) ad = 0; - - if (sweepFlag == 0 && ad > 0) ad = ad - 2 * Math.PI; - if (sweepFlag == 1 && ad < 0) ad = ad + 2 * Math.PI; - - // for markers - var halfWay = new svg.Point( - centp.x - rx * Math.cos((a1 + ad) / 2), - centp.y - ry * Math.sin((a1 + ad) / 2) - ); - pp.addMarkerAngle(halfWay, (a1 + ad) / 2 + (sweepFlag == 0 ? 1 : -1) * Math.PI / 2); - pp.addMarkerAngle(cp, ad + (sweepFlag == 0 ? 1 : -1) * Math.PI / 2); - - bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better - if (ctx != null) { - var r = rx > ry ? rx : ry; - var sx = rx > ry ? 1 : rx / ry; - var sy = rx > ry ? ry / rx : 1; - - ctx.translate(centp.x, centp.y); - ctx.rotate(xAxisRotation); - ctx.scale(sx, sy); - ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); - ctx.scale(1/sx, 1/sy); - ctx.rotate(-xAxisRotation); - ctx.translate(-centp.x, -centp.y); - } - } - break; - case 'Z': - if (ctx != null) ctx.closePath(); - pp.current = pp.start; - } - } - - return bb; - } - - this.getMarkers = function() { - var points = this.PathParser.getMarkerPoints(); - var angles = this.PathParser.getMarkerAngles(); - - var markers = []; - for (var i=0; i this.maxDuration) { - // loop for indefinitely repeating animations - if (this.attribute('repeatCount').value == 'indefinite') { - this.duration = 0.0 - } - else if (this.attribute('fill').valueOrDefault('remove') == 'remove' && !this.removed) { - this.removed = true; - this.getProperty().value = this.initialValue; - return true; - } - else { - return false; // no updates made - } - } - this.duration = this.duration + delta; - - // if we're past the begin time - var updated = false; - if (this.begin < this.duration) { - var newValue = this.calcValue(); // tween - - if (this.attribute('type').hasValue()) { - // for transform, etc. - var type = this.attribute('type').value; - newValue = type + '(' + newValue + ')'; - } - - this.getProperty().value = newValue; - updated = true; - } - - return updated; - } - - // fraction of duration we've covered - this.progress = function() { - return ((this.duration - this.begin) / (this.maxDuration - this.begin)); - } - } - svg.Element.AnimateBase.prototype = new svg.Element.ElementBase; - - // animate element - svg.Element.animate = function(node) { - this.base = svg.Element.AnimateBase; - this.base(node); - - this.calcValue = function() { - var from = this.attribute('from').numValue(); - var to = this.attribute('to').numValue(); - - // tween value linearly - return from + (to - from) * this.progress(); - }; - } - svg.Element.animate.prototype = new svg.Element.AnimateBase; - - // animate color element - svg.Element.animateColor = function(node) { - this.base = svg.Element.AnimateBase; - this.base(node); - - this.calcValue = function() { - var from = new RGBColor(this.attribute('from').value); - var to = new RGBColor(this.attribute('to').value); - - if (from.ok && to.ok) { - // tween color linearly - var r = from.r + (to.r - from.r) * this.progress(); - var g = from.g + (to.g - from.g) * this.progress(); - var b = from.b + (to.b - from.b) * this.progress(); - return 'rgb('+parseInt(r,10)+','+parseInt(g,10)+','+parseInt(b,10)+')'; - } - return this.attribute('from').value; - }; - } - svg.Element.animateColor.prototype = new svg.Element.AnimateBase; - - // animate transform element - svg.Element.animateTransform = function(node) { - this.base = svg.Element.animate; - this.base(node); - } - svg.Element.animateTransform.prototype = new svg.Element.animate; - - // font element - svg.Element.font = function(node) { - this.base = svg.Element.ElementBase; - this.base(node); - - this.horizAdvX = this.attribute('horiz-adv-x').numValue(); - - this.isRTL = false; - this.isArabic = false; - this.fontFace = null; - this.missingGlyph = null; - this.glyphs = []; - for (var i=0; i0 && text[i-1]!=' ' && i0 && text[i-1]!=' ' && (i == text.length-1 || text[i+1]==' ')) arabicForm = 'initial'; - if (typeof(font.glyphs[c]) != 'undefined') { - glyph = font.glyphs[c][arabicForm]; - if (glyph == null && font.glyphs[c].type == 'glyph') glyph = font.glyphs[c]; - } - } - else { - glyph = font.glyphs[c]; - } - if (glyph == null) glyph = font.missingGlyph; - return glyph; - } - - this.renderChildren = function(ctx) { - var customFont = this.parent.style('font-family').Definition.getDefinition(); - if (customFont != null) { - var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); - var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); - var text = this.getText(); - if (customFont.isRTL) text = text.split("").reverse().join(""); - - var dx = svg.ToNumberArray(this.parent.attribute('dx').value); - for (var i=0; i 0 ? node.childNodes[0].nodeValue : // element - node.text; - this.getText = function() { - return this.text; - } - } - svg.Element.tspan.prototype = new svg.Element.TextElementBase; - - // tref - svg.Element.tref = function(node) { - this.base = svg.Element.TextElementBase; - this.base(node); - - this.getText = function() { - var element = this.attribute('xlink:href').Definition.getDefinition(); - if (element != null) return element.children[0].getText(); - } - } - svg.Element.tref.prototype = new svg.Element.TextElementBase; - - // a element - svg.Element.a = function(node) { - this.base = svg.Element.TextElementBase; - this.base(node); - - this.hasText = true; - for (var i=0; i 1 ? node.childNodes[1].nodeValue : ''); - css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // remove comments - css = svg.compressSpaces(css); // replace whitespace - var cssDefs = css.split('}'); - for (var i=0; i 0) { - var urlStart = srcs[s].indexOf('url'); - var urlEnd = srcs[s].indexOf(')', urlStart); - var url = srcs[s].substr(urlStart + 5, urlEnd - urlStart - 6); - var doc = svg.parseXml(svg.ajax(url)); - var fonts = doc.getElementsByTagName('font'); - for (var f=0; f - * @link http://www.phpied.com/rgb-color-parser-in-javascript/ - * @license Use it if you like it - */ -function RGBColor(color_string) -{ - this.ok = false; - - // strip any leading # - if (color_string.charAt(0) == '#') { // remove # if any - color_string = color_string.substr(1,6); - } - - color_string = color_string.replace(/ /g,''); - color_string = color_string.toLowerCase(); - - // before getting into regexps, try simple matches - // and overwrite the input - var simple_colors = { - aliceblue: 'f0f8ff', - antiquewhite: 'faebd7', - aqua: '00ffff', - aquamarine: '7fffd4', - azure: 'f0ffff', - beige: 'f5f5dc', - bisque: 'ffe4c4', - black: '000000', - blanchedalmond: 'ffebcd', - blue: '0000ff', - blueviolet: '8a2be2', - brown: 'a52a2a', - burlywood: 'deb887', - cadetblue: '5f9ea0', - chartreuse: '7fff00', - chocolate: 'd2691e', - coral: 'ff7f50', - cornflowerblue: '6495ed', - cornsilk: 'fff8dc', - crimson: 'dc143c', - cyan: '00ffff', - darkblue: '00008b', - darkcyan: '008b8b', - darkgoldenrod: 'b8860b', - darkgray: 'a9a9a9', - darkgreen: '006400', - darkkhaki: 'bdb76b', - darkmagenta: '8b008b', - darkolivegreen: '556b2f', - darkorange: 'ff8c00', - darkorchid: '9932cc', - darkred: '8b0000', - darksalmon: 'e9967a', - darkseagreen: '8fbc8f', - darkslateblue: '483d8b', - darkslategray: '2f4f4f', - darkturquoise: '00ced1', - darkviolet: '9400d3', - deeppink: 'ff1493', - deepskyblue: '00bfff', - dimgray: '696969', - dodgerblue: '1e90ff', - feldspar: 'd19275', - firebrick: 'b22222', - floralwhite: 'fffaf0', - forestgreen: '228b22', - fuchsia: 'ff00ff', - gainsboro: 'dcdcdc', - ghostwhite: 'f8f8ff', - gold: 'ffd700', - goldenrod: 'daa520', - gray: '808080', - green: '008000', - greenyellow: 'adff2f', - honeydew: 'f0fff0', - hotpink: 'ff69b4', - indianred : 'cd5c5c', - indigo : '4b0082', - ivory: 'fffff0', - khaki: 'f0e68c', - lavender: 'e6e6fa', - lavenderblush: 'fff0f5', - lawngreen: '7cfc00', - lemonchiffon: 'fffacd', - lightblue: 'add8e6', - lightcoral: 'f08080', - lightcyan: 'e0ffff', - lightgoldenrodyellow: 'fafad2', - lightgrey: 'd3d3d3', - lightgreen: '90ee90', - lightpink: 'ffb6c1', - lightsalmon: 'ffa07a', - lightseagreen: '20b2aa', - lightskyblue: '87cefa', - lightslateblue: '8470ff', - lightslategray: '778899', - lightsteelblue: 'b0c4de', - lightyellow: 'ffffe0', - lime: '00ff00', - limegreen: '32cd32', - linen: 'faf0e6', - magenta: 'ff00ff', - maroon: '800000', - mediumaquamarine: '66cdaa', - mediumblue: '0000cd', - mediumorchid: 'ba55d3', - mediumpurple: '9370d8', - mediumseagreen: '3cb371', - mediumslateblue: '7b68ee', - mediumspringgreen: '00fa9a', - mediumturquoise: '48d1cc', - mediumvioletred: 'c71585', - midnightblue: '191970', - mintcream: 'f5fffa', - mistyrose: 'ffe4e1', - moccasin: 'ffe4b5', - navajowhite: 'ffdead', - navy: '000080', - oldlace: 'fdf5e6', - olive: '808000', - olivedrab: '6b8e23', - orange: 'ffa500', - orangered: 'ff4500', - orchid: 'da70d6', - palegoldenrod: 'eee8aa', - palegreen: '98fb98', - paleturquoise: 'afeeee', - palevioletred: 'd87093', - papayawhip: 'ffefd5', - peachpuff: 'ffdab9', - peru: 'cd853f', - pink: 'ffc0cb', - plum: 'dda0dd', - powderblue: 'b0e0e6', - purple: '800080', - red: 'ff0000', - rosybrown: 'bc8f8f', - royalblue: '4169e1', - saddlebrown: '8b4513', - salmon: 'fa8072', - sandybrown: 'f4a460', - seagreen: '2e8b57', - seashell: 'fff5ee', - sienna: 'a0522d', - silver: 'c0c0c0', - skyblue: '87ceeb', - slateblue: '6a5acd', - slategray: '708090', - snow: 'fffafa', - springgreen: '00ff7f', - steelblue: '4682b4', - tan: 'd2b48c', - teal: '008080', - thistle: 'd8bfd8', - tomato: 'ff6347', - turquoise: '40e0d0', - violet: 'ee82ee', - violetred: 'd02090', - wheat: 'f5deb3', - white: 'ffffff', - whitesmoke: 'f5f5f5', - yellow: 'ffff00', - yellowgreen: '9acd32' - }; - for (var key in simple_colors) { - if (color_string == key) { - color_string = simple_colors[key]; - } - } - // emd of simple type-in colors - - // array of color definition objects - var color_defs = [ - { - re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, - example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], - process: function (bits){ - return [ - parseInt(bits[1]), - parseInt(bits[2]), - parseInt(bits[3]) - ]; - } - }, - { - re: /^(\w{2})(\w{2})(\w{2})$/, - example: ['#00ff00', '336699'], - process: function (bits){ - return [ - parseInt(bits[1], 16), - parseInt(bits[2], 16), - parseInt(bits[3], 16) - ]; - } - }, - { - re: /^(\w{1})(\w{1})(\w{1})$/, - example: ['#fb0', 'f0f'], - process: function (bits){ - return [ - parseInt(bits[1] + bits[1], 16), - parseInt(bits[2] + bits[2], 16), - parseInt(bits[3] + bits[3], 16) - ]; - } - } - ]; - - // search through the definitions to find a match - for (var i = 0; i < color_defs.length; i++) { - var re = color_defs[i].re; - var processor = color_defs[i].process; - var bits = re.exec(color_string); - if (bits) { - channels = processor(bits); - this.r = channels[0]; - this.g = channels[1]; - this.b = channels[2]; - this.ok = true; - } - - } - - // validate/cleanup values - this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); - this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); - this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); - - // some getters - this.toRGB = function () { - return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; - } - this.toHex = function () { - var r = this.r.toString(16); - var g = this.g.toString(16); - var b = this.b.toString(16); - if (r.length == 1) r = '0' + r; - if (g.length == 1) g = '0' + g; - if (b.length == 1) b = '0' + b; - return '#' + r + g + b; - } - - // help - this.getHelpXML = function () { - - var examples = new Array(); - // add regexps - for (var i = 0; i < color_defs.length; i++) { - var example = color_defs[i].example; - for (var j = 0; j < example.length; j++) { - examples[examples.length] = example[j]; - } - } - // add type-in colors - for (var sc in simple_colors) { - examples[examples.length] = sc; - } - - var xml = document.createElement('ul'); - xml.setAttribute('id', 'rgbcolor-examples'); - for (var i = 0; i < examples.length; i++) { - try { - var list_item = document.createElement('li'); - var list_color = new RGBColor(examples[i]); - var example_div = document.createElement('div'); - example_div.style.cssText = - 'margin: 3px; ' - + 'border: 1px solid black; ' - + 'background:' + list_color.toHex() + '; ' - + 'color:' + list_color.toHex() - ; - example_div.appendChild(document.createTextNode('test')); - var list_item_value = document.createTextNode( - ' ' + examples[i] + ' -> ' + list_color.toRGB() + ' -> ' + list_color.toHex() - ); - list_item.appendChild(example_div); - list_item.appendChild(list_item_value); - xml.appendChild(list_item); - - } catch(e){} - } - return xml; - - } - -} diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/contextmenu.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/contextmenu.js deleted file mode 100644 index 0d5dd343..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/contextmenu.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Package: svgedit.contextmenu - * - * Licensed under the Apache License, Version 2 - * - * Author: Adam Bender - */ -// Dependencies: -// 1) jQuery (for dom injection of context menus) -var svgedit = svgedit || {}; -(function() { - var self = this; - if (!svgedit.contextmenu) { - svgedit.contextmenu = {}; - } - self.contextMenuExtensions = {} - var addContextMenuItem = function(menuItem) { - // menuItem: {id, label, shortcut, action} - if (!menuItemIsValid(menuItem)) { - console - .error("Menu items must be defined and have at least properties: id, label, action, where action must be a function"); - return; - } - if (menuItem.id in self.contextMenuExtensions) { - console.error('Cannot add extension "' + menuItem.id - + '", an extension by that name already exists"'); - return; - } - // Register menuItem action, see below for deferred menu dom injection - console.log("Registed contextmenu item: {id:"+ menuItem.id+", label:"+menuItem.label+"}"); - self.contextMenuExtensions[menuItem.id] = menuItem; - //TODO: Need to consider how to handle custom enable/disable behavior - } - var hasCustomHandler = function(handlerKey) { - return self.contextMenuExtensions[handlerKey] && true; - } - var getCustomHandler = function(handlerKey) { - return self.contextMenuExtensions[handlerKey].action; - } - var injectExtendedContextMenuItemIntoDom = function(menuItem) { - if (Object.keys(self.contextMenuExtensions).length == 0) { - // all menuItems appear at the bottom of the menu in their own container. - // if this is the first extension menu we need to add the separator. - $("#cmenu_canvas").append("
  • "); - } - var shortcut = menuItem.shortcut || ""; - $("#cmenu_canvas").append("
  • " - + menuItem.label + "" - + shortcut + "
  • "); - } - - var menuItemIsValid = function(menuItem) { - return menuItem && menuItem.id && menuItem.label && menuItem.action && typeof menuItem.action == 'function'; - } - - // Defer injection to wait out initial menu processing. This probably goes away once all context - // menu behavior is brought here. - svgEditor.ready(function() { - for (menuItem in contextMenuExtensions) { - injectExtendedContextMenuItemIntoDom(contextMenuExtensions[menuItem]); - } - }); - svgedit.contextmenu.resetCustomMenus = function(){self.contextMenuExtensions = {}} - svgedit.contextmenu.add = addContextMenuItem; - svgedit.contextmenu.hasCustomHandler = hasCustomHandler; - svgedit.contextmenu.getCustomHandler = getCustomHandler; -})(); diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/contextmenu/jquery.contextMenu.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/contextmenu/jquery.contextMenu.js deleted file mode 100755 index 76126019..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/contextmenu/jquery.contextMenu.js +++ /dev/null @@ -1,203 +0,0 @@ -// jQuery Context Menu Plugin -// -// Version 1.01 -// -// Cory S.N. LaViska -// A Beautiful Site (http://abeautifulsite.net/) -// Modified by Alexis Deveria -// -// More info: http://abeautifulsite.net/2008/09/jquery-context-menu-plugin/ -// -// Terms of Use -// -// This plugin is dual-licensed under the GNU General Public License -// and the MIT License and is copyright A Beautiful Site, LLC. -// -if(jQuery)( function() { - var win = $(window); - var doc = $(document); - - $.extend($.fn, { - - contextMenu: function(o, callback) { - // Defaults - if( o.menu == undefined ) return false; - if( o.inSpeed == undefined ) o.inSpeed = 150; - if( o.outSpeed == undefined ) o.outSpeed = 75; - // 0 needs to be -1 for expected results (no fade) - if( o.inSpeed == 0 ) o.inSpeed = -1; - if( o.outSpeed == 0 ) o.outSpeed = -1; - // Loop each context menu - $(this).each( function() { - var el = $(this); - var offset = $(el).offset(); - - var menu = $('#' + o.menu); - - // Add contextMenu class - menu.addClass('contextMenu'); - // Simulate a true right click - $(this).bind( "mousedown", function(e) { - var evt = e; - $(this).mouseup( function(e) { - var srcElement = $(this); - srcElement.unbind('mouseup'); - if( evt.button === 2 || o.allowLeft || (evt.ctrlKey && svgedit.browser.isMac()) ) { - e.stopPropagation(); - // Hide context menus that may be showing - $(".contextMenu").hide(); - // Get this context menu - - if( el.hasClass('disabled') ) return false; - - // Detect mouse position - var d = {}, x = e.pageX, y = e.pageY; - - var x_off = win.width() - menu.width(), - y_off = win.height() - menu.height(); - - if(x > x_off - 15) x = x_off-15; - if(y > y_off - 30) y = y_off-30; // 30 is needed to prevent scrollbars in FF - - // Show the menu - doc.unbind('click'); - menu.css({ top: y, left: x }).fadeIn(o.inSpeed); - // Hover events - menu.find('A').mouseover( function() { - menu.find('LI.hover').removeClass('hover'); - $(this).parent().addClass('hover'); - }).mouseout( function() { - menu.find('LI.hover').removeClass('hover'); - }); - - // Keyboard - doc.keypress( function(e) { - switch( e.keyCode ) { - case 38: // up - if( !menu.find('LI.hover').length ) { - menu.find('LI:last').addClass('hover'); - } else { - menu.find('LI.hover').removeClass('hover').prevAll('LI:not(.disabled)').eq(0).addClass('hover'); - if( !menu.find('LI.hover').length ) menu.find('LI:last').addClass('hover'); - } - break; - case 40: // down - if( menu.find('LI.hover').length == 0 ) { - menu.find('LI:first').addClass('hover'); - } else { - menu.find('LI.hover').removeClass('hover').nextAll('LI:not(.disabled)').eq(0).addClass('hover'); - if( !menu.find('LI.hover').length ) menu.find('LI:first').addClass('hover'); - } - break; - case 13: // enter - menu.find('LI.hover A').trigger('click'); - break; - case 27: // esc - doc.trigger('click'); - break - } - }); - - // When items are selected - menu.find('A').unbind('mouseup'); - menu.find('LI:not(.disabled) A').mouseup( function() { - doc.unbind('click').unbind('keypress'); - $(".contextMenu").hide(); - // Callback - if( callback ) callback( $(this).attr('href').substr(1), $(srcElement), {x: x - offset.left, y: y - offset.top, docX: x, docY: y} ); - return false; - }); - - // Hide bindings - setTimeout( function() { // Delay for Mozilla - doc.click( function() { - doc.unbind('click').unbind('keypress'); - menu.fadeOut(o.outSpeed); - return false; - }); - }, 0); - } - }); - }); - - // Disable text selection - if( $.browser.mozilla ) { - $('#' + o.menu).each( function() { $(this).css({ 'MozUserSelect' : 'none' }); }); - } else if( $.browser.msie ) { - $('#' + o.menu).each( function() { $(this).bind('selectstart.disableTextSelect', function() { return false; }); }); - } else { - $('#' + o.menu).each(function() { $(this).bind('mousedown.disableTextSelect', function() { return false; }); }); - } - // Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome) - $(el).add($('UL.contextMenu')).bind('contextmenu', function() { return false; }); - - }); - return $(this); - }, - - // Disable context menu items on the fly - disableContextMenuItems: function(o) { - if( o == undefined ) { - // Disable all - $(this).find('LI').addClass('disabled'); - return( $(this) ); - } - $(this).each( function() { - if( o != undefined ) { - var d = o.split(','); - for( var i = 0; i < d.length; i++ ) { - $(this).find('A[href="' + d[i] + '"]').parent().addClass('disabled'); - - } - } - }); - return( $(this) ); - }, - - // Enable context menu items on the fly - enableContextMenuItems: function(o) { - if( o == undefined ) { - // Enable all - $(this).find('LI.disabled').removeClass('disabled'); - return( $(this) ); - } - $(this).each( function() { - if( o != undefined ) { - var d = o.split(','); - for( var i = 0; i < d.length; i++ ) { - $(this).find('A[href="' + d[i] + '"]').parent().removeClass('disabled'); - - } - } - }); - return( $(this) ); - }, - - // Disable context menu(s) - disableContextMenu: function() { - $(this).each( function() { - $(this).addClass('disabled'); - }); - return( $(this) ); - }, - - // Enable context menu(s) - enableContextMenu: function() { - $(this).each( function() { - $(this).removeClass('disabled'); - }); - return( $(this) ); - }, - - // Destroy context menu(s) - destroyContextMenu: function() { - // Destroy specified context menus - $(this).each( function() { - // Disable action - $(this).unbind('mousedown').unbind('mouseup'); - }); - return( $(this) ); - } - - }); -})(jQuery); \ No newline at end of file diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/draw.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/draw.js deleted file mode 100644 index 3805164a..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/draw.js +++ /dev/null @@ -1,528 +0,0 @@ -/** - * Package: svgedit.draw - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Jeff Schiller - */ - -// Dependencies: -// 1) jQuery -// 2) browser.js -// 3) svgutils.js - -var svgedit = svgedit || {}; - -(function() { - -if (!svgedit.draw) { - svgedit.draw = {}; -} - -var svg_ns = "http://www.w3.org/2000/svg"; -var se_ns = "http://svg-edit.googlecode.com"; -var xmlns_ns = "http://www.w3.org/2000/xmlns/"; - -var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'; -var visElems_arr = visElems.split(','); - -var RandomizeModes = { - LET_DOCUMENT_DECIDE: 0, - ALWAYS_RANDOMIZE: 1, - NEVER_RANDOMIZE: 2 -}; -var randomize_ids = RandomizeModes.LET_DOCUMENT_DECIDE; - -/** - * This class encapsulates the concept of a layer in the drawing - * @param name {String} Layer name - * @param child {SVGGElement} Layer SVG group. - */ -svgedit.draw.Layer = function(name, group) { - this.name_ = name; - this.group_ = group; -}; - -svgedit.draw.Layer.prototype.getName = function() { - return this.name_; -}; - -svgedit.draw.Layer.prototype.getGroup = function() { - return this.group_; -}; - - -// Called to ensure that drawings will or will not have randomized ids. -// The current_drawing will have its nonce set if it doesn't already. -// -// Params: -// enableRandomization - flag indicating if documents should have randomized ids -svgedit.draw.randomizeIds = function(enableRandomization, current_drawing) { - randomize_ids = enableRandomization == false ? - RandomizeModes.NEVER_RANDOMIZE : - RandomizeModes.ALWAYS_RANDOMIZE; - - if (randomize_ids == RandomizeModes.ALWAYS_RANDOMIZE && !current_drawing.getNonce()) { - current_drawing.setNonce(Math.floor(Math.random() * 100001)); - } else if (randomize_ids == RandomizeModes.NEVER_RANDOMIZE && current_drawing.getNonce()) { - current_drawing.clearNonce(); - } -}; - -/** - * This class encapsulates the concept of a SVG-edit drawing - * - * @param svgElem {SVGSVGElement} The SVG DOM Element that this JS object - * encapsulates. If the svgElem has a se:nonce attribute on it, then - * IDs will use the nonce as they are generated. - * @param opt_idPrefix {String} The ID prefix to use. Defaults to "svg_" - * if not specified. - */ -svgedit.draw.Drawing = function(svgElem, opt_idPrefix) { - if (!svgElem || !svgElem.tagName || !svgElem.namespaceURI || - svgElem.tagName != 'svg' || svgElem.namespaceURI != svg_ns) { - throw "Error: svgedit.draw.Drawing instance initialized without a element"; - } - - /** - * The SVG DOM Element that represents this drawing. - * @type {SVGSVGElement} - */ - this.svgElem_ = svgElem; - - /** - * The latest object number used in this drawing. - * @type {number} - */ - this.obj_num = 0; - - /** - * The prefix to prepend to each element id in the drawing. - * @type {String} - */ - this.idPrefix = opt_idPrefix || "svg_"; - - /** - * An array of released element ids to immediately reuse. - * @type {Array.} - */ - this.releasedNums = []; - - /** - * The z-ordered array of tuples containing layer names and elements. - * The first layer is the one at the bottom of the rendering. - * TODO: Turn this into an Array. - * @type {Array.>} - */ - this.all_layers = []; - - /** - * The current layer being used. - * TODO: Make this a {Layer}. - * @type {SVGGElement} - */ - this.current_layer = null; - - /** - * The nonce to use to uniquely identify elements across drawings. - * @type {!String} - */ - this.nonce_ = ""; - var n = this.svgElem_.getAttributeNS(se_ns, 'nonce'); - // If already set in the DOM, use the nonce throughout the document - // else, if randomizeIds(true) has been called, create and set the nonce. - if (!!n && randomize_ids != RandomizeModes.NEVER_RANDOMIZE) { - this.nonce_ = n; - } else if (randomize_ids == RandomizeModes.ALWAYS_RANDOMIZE) { - this.setNonce(Math.floor(Math.random() * 100001)); - } -}; - -svgedit.draw.Drawing.prototype.getElem_ = function(id) { - if(this.svgElem_.querySelector) { - // querySelector lookup - return this.svgElem_.querySelector('#'+id); - } else { - // jQuery lookup: twice as slow as xpath in FF - return $(this.svgElem_).find('[id=' + id + ']')[0]; - } -}; - -svgedit.draw.Drawing.prototype.getSvgElem = function() { - return this.svgElem_; -}; - -svgedit.draw.Drawing.prototype.getNonce = function() { - return this.nonce_; -}; - -svgedit.draw.Drawing.prototype.setNonce = function(n) { - this.svgElem_.setAttributeNS(xmlns_ns, 'xmlns:se', se_ns); - this.svgElem_.setAttributeNS(se_ns, 'se:nonce', n); - this.nonce_ = n; -}; - -svgedit.draw.Drawing.prototype.clearNonce = function() { - // We deliberately leave any se:nonce attributes alone, - // we just don't use it to randomize ids. - this.nonce_ = ""; -}; - -/** - * Returns the latest object id as a string. - * @return {String} The latest object Id. - */ -svgedit.draw.Drawing.prototype.getId = function() { - return this.nonce_ ? - this.idPrefix + this.nonce_ +'_' + this.obj_num : - this.idPrefix + this.obj_num; -}; - -/** - * Returns the next object Id as a string. - * @return {String} The next object Id to use. - */ -svgedit.draw.Drawing.prototype.getNextId = function() { - var oldObjNum = this.obj_num; - var restoreOldObjNum = false; - - // If there are any released numbers in the release stack, - // use the last one instead of the next obj_num. - // We need to temporarily use obj_num as that is what getId() depends on. - if (this.releasedNums.length > 0) { - this.obj_num = this.releasedNums.pop(); - restoreOldObjNum = true; - } else { - // If we are not using a released id, then increment the obj_num. - this.obj_num++; - } - - // Ensure the ID does not exist. - var id = this.getId(); - while (this.getElem_(id)) { - if (restoreOldObjNum) { - this.obj_num = oldObjNum; - restoreOldObjNum = false; - } - this.obj_num++; - id = this.getId(); - } - // Restore the old object number if required. - if (restoreOldObjNum) { - this.obj_num = oldObjNum; - } - return id; -}; - -// Function: svgedit.draw.Drawing.releaseId -// Releases the object Id, letting it be used as the next id in getNextId(). -// This method DOES NOT remove any elements from the DOM, it is expected -// that client code will do this. -// -// Parameters: -// id - The id to release. -// -// Returns: -// True if the id was valid to be released, false otherwise. -svgedit.draw.Drawing.prototype.releaseId = function(id) { - // confirm if this is a valid id for this Document, else return false - var front = this.idPrefix + (this.nonce_ ? this.nonce_ +'_' : ''); - if (typeof id != typeof '' || id.indexOf(front) != 0) { - return false; - } - // extract the obj_num of this id - var num = parseInt(id.substr(front.length)); - - // if we didn't get a positive number or we already released this number - // then return false. - if (typeof num != typeof 1 || num <= 0 || this.releasedNums.indexOf(num) != -1) { - return false; - } - - // push the released number into the released queue - this.releasedNums.push(num); - - return true; -}; - -// Function: svgedit.draw.Drawing.getNumLayers -// Returns the number of layers in the current drawing. -// -// Returns: -// The number of layers in the current drawing. -svgedit.draw.Drawing.prototype.getNumLayers = function() { - return this.all_layers.length; -}; - -// Function: svgedit.draw.Drawing.hasLayer -// Check if layer with given name already exists -svgedit.draw.Drawing.prototype.hasLayer = function(name) { - for(var i = 0; i < this.getNumLayers(); i++) { - if(this.all_layers[i][0] == name) return true; - } - return false; -}; - - -// Function: svgedit.draw.Drawing.getLayerName -// Returns the name of the ith layer. If the index is out of range, an empty string is returned. -// -// Parameters: -// i - the zero-based index of the layer you are querying. -// -// Returns: -// The name of the ith layer -svgedit.draw.Drawing.prototype.getLayerName = function(i) { - if (i >= 0 && i < this.getNumLayers()) { - return this.all_layers[i][0]; - } - return ""; -}; - -// Function: svgedit.draw.Drawing.getCurrentLayer -// Returns: -// The SVGGElement representing the current layer. -svgedit.draw.Drawing.prototype.getCurrentLayer = function() { - return this.current_layer; -}; - -// Function: getCurrentLayerName -// Returns the name of the currently selected layer. If an error occurs, an empty string -// is returned. -// -// Returns: -// The name of the currently active layer. -svgedit.draw.Drawing.prototype.getCurrentLayerName = function() { - for (var i = 0; i < this.getNumLayers(); ++i) { - if (this.all_layers[i][1] == this.current_layer) { - return this.getLayerName(i); - } - } - return ""; -}; - -// Function: setCurrentLayer -// Sets the current layer. If the name is not a valid layer name, then this function returns -// false. Otherwise it returns true. This is not an undo-able action. -// -// Parameters: -// name - the name of the layer you want to switch to. -// -// Returns: -// true if the current layer was switched, otherwise false -svgedit.draw.Drawing.prototype.setCurrentLayer = function(name) { - for (var i = 0; i < this.getNumLayers(); ++i) { - if (name == this.getLayerName(i)) { - if (this.current_layer != this.all_layers[i][1]) { - this.current_layer.setAttribute("style", "pointer-events:none"); - this.current_layer = this.all_layers[i][1]; - this.current_layer.setAttribute("style", "pointer-events:all"); - } - return true; - } - } - return false; -}; - - -// Function: svgedit.draw.Drawing.deleteCurrentLayer -// Deletes the current layer from the drawing and then clears the selection. This function -// then calls the 'changed' handler. This is an undoable action. -// Returns: -// The SVGGElement of the layer removed or null. -svgedit.draw.Drawing.prototype.deleteCurrentLayer = function() { - if (this.current_layer && this.getNumLayers() > 1) { - // actually delete from the DOM and return it - var parent = this.current_layer.parentNode; - var nextSibling = this.current_layer.nextSibling; - var oldLayerGroup = parent.removeChild(this.current_layer); - this.identifyLayers(); - return oldLayerGroup; - } - return null; -}; - -// Function: svgedit.draw.Drawing.identifyLayers -// Updates layer system and sets the current layer to the -// top-most layer (last child of this drawing). -svgedit.draw.Drawing.prototype.identifyLayers = function() { - this.all_layers = []; - var numchildren = this.svgElem_.childNodes.length; - // loop through all children of SVG element - var orphans = [], layernames = []; - var a_layer = null; - var childgroups = false; - for (var i = 0; i < numchildren; ++i) { - var child = this.svgElem_.childNodes.item(i); - // for each g, find its layer name - if (child && child.nodeType == 1) { - if (child.tagName == "g") { - childgroups = true; - var name = $("title",child).text(); - - // Hack for Opera 10.60 - if(!name && svgedit.browser.isOpera() && child.querySelectorAll) { - name = $(child.querySelectorAll('title')).text(); - } - - // store layer and name in global variable - if (name) { - layernames.push(name); - this.all_layers.push( [name,child] ); - a_layer = child; - svgedit.utilities.walkTree(child, function(e){e.setAttribute("style", "pointer-events:inherit");}); - a_layer.setAttribute("style", "pointer-events:none"); - } - // if group did not have a name, it is an orphan - else { - orphans.push(child); - } - } - // if child has is "visible" (i.e. not a or element), then it is an orphan - else if(~visElems_arr.indexOf(child.nodeName)) { - var bb = svgedit.utilities.getBBox(child); - orphans.push(child); - } - } - } - - // create a new layer and add all the orphans to it - var svgdoc = this.svgElem_.ownerDocument; - if (orphans.length > 0 || !childgroups) { - var i = 1; - // TODO(codedread): What about internationalization of "Layer"? - while (layernames.indexOf(("Layer " + i)) >= 0) { i++; } - var newname = "Layer " + i; - a_layer = svgdoc.createElementNS(svg_ns, "g"); - var layer_title = svgdoc.createElementNS(svg_ns, "title"); - layer_title.textContent = newname; - a_layer.appendChild(layer_title); - for (var j = 0; j < orphans.length; ++j) { - a_layer.appendChild(orphans[j]); - } - this.svgElem_.appendChild(a_layer); - this.all_layers.push( [newname, a_layer] ); - } - svgedit.utilities.walkTree(a_layer, function(e){e.setAttribute("style","pointer-events:inherit");}); - this.current_layer = a_layer; - this.current_layer.setAttribute("style","pointer-events:all"); -}; - -// Function: svgedit.draw.Drawing.createLayer -// Creates a new top-level layer in the drawing with the given name and -// sets the current layer to it. -// -// Parameters: -// name - The given name -// -// Returns: -// The SVGGElement of the new layer, which is also the current layer -// of this drawing. -svgedit.draw.Drawing.prototype.createLayer = function(name) { - var svgdoc = this.svgElem_.ownerDocument; - var new_layer = svgdoc.createElementNS(svg_ns, "g"); - var layer_title = svgdoc.createElementNS(svg_ns, "title"); - layer_title.textContent = name; - new_layer.appendChild(layer_title); - this.svgElem_.appendChild(new_layer); - this.identifyLayers(); - return new_layer; -}; - -// Function: svgedit.draw.Drawing.getLayerVisibility -// Returns whether the layer is visible. If the layer name is not valid, then this function -// returns false. -// -// Parameters: -// layername - the name of the layer which you want to query. -// -// Returns: -// The visibility state of the layer, or false if the layer name was invalid. -svgedit.draw.Drawing.prototype.getLayerVisibility = function(layername) { - // find the layer - var layer = null; - for (var i = 0; i < this.getNumLayers(); ++i) { - if (this.getLayerName(i) == layername) { - layer = this.all_layers[i][1]; - break; - } - } - if (!layer) return false; - return (layer.getAttribute('display') != 'none'); -}; - -// Function: svgedit.draw.Drawing.setLayerVisibility -// Sets the visibility of the layer. If the layer name is not valid, this function return -// false, otherwise it returns true. This is an undo-able action. -// -// Parameters: -// layername - the name of the layer to change the visibility -// bVisible - true/false, whether the layer should be visible -// -// Returns: -// The SVGGElement representing the layer if the layername was valid, otherwise null. -svgedit.draw.Drawing.prototype.setLayerVisibility = function(layername, bVisible) { - if (typeof bVisible != typeof true) { - return null; - } - // find the layer - var layer = null; - for (var i = 0; i < this.getNumLayers(); ++i) { - if (this.getLayerName(i) == layername) { - layer = this.all_layers[i][1]; - break; - } - } - if (!layer) return null; - - var oldDisplay = layer.getAttribute("display"); - if (!oldDisplay) oldDisplay = "inline"; - layer.setAttribute("display", bVisible ? "inline" : "none"); - return layer; -}; - - -// Function: svgedit.draw.Drawing.getLayerOpacity -// Returns the opacity of the given layer. If the input name is not a layer, null is returned. -// -// Parameters: -// layername - name of the layer on which to get the opacity -// -// Returns: -// The opacity value of the given layer. This will be a value between 0.0 and 1.0, or null -// if layername is not a valid layer -svgedit.draw.Drawing.prototype.getLayerOpacity = function(layername) { - for (var i = 0; i < this.getNumLayers(); ++i) { - if (this.getLayerName(i) == layername) { - var g = this.all_layers[i][1]; - var opacity = g.getAttribute('opacity'); - if (!opacity) { - opacity = '1.0'; - } - return parseFloat(opacity); - } - } - return null; -}; - -// Function: svgedit.draw.Drawing.setLayerOpacity -// Sets the opacity of the given layer. If the input name is not a layer, nothing happens. -// If opacity is not a value between 0.0 and 1.0, then nothing happens. -// -// Parameters: -// layername - name of the layer on which to set the opacity -// opacity - a float value in the range 0.0-1.0 -svgedit.draw.Drawing.prototype.setLayerOpacity = function(layername, opacity) { - if (typeof opacity != typeof 1.0 || opacity < 0.0 || opacity > 1.0) { - return; - } - for (var i = 0; i < this.getNumLayers(); ++i) { - if (this.getLayerName(i) == layername) { - var g = this.all_layers[i][1]; - g.setAttribute("opacity", opacity); - break; - } - } -}; - -})(); diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/embedapi.html b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/embedapi.html deleted file mode 100644 index 3db03640..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/embedapi.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - -
    - - - - diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/embedapi.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/embedapi.js deleted file mode 100644 index d4c15392..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/embedapi.js +++ /dev/null @@ -1,173 +0,0 @@ -/* -function embedded_svg_edit(frame){ - //initialize communication - this.frame = frame; - this.stack = []; //callback stack - - var editapi = this; - - window.addEventListener("message", function(e){ - if(e.data.substr(0,5) == "ERROR"){ - editapi.stack.splice(0,1)[0](e.data,"error") - }else{ - editapi.stack.splice(0,1)[0](e.data) - } - }, false) -} - -embedded_svg_edit.prototype.call = function(code, callback){ - this.stack.push(callback); - this.frame.contentWindow.postMessage(code,"*"); -} - -embedded_svg_edit.prototype.getSvgString = function(callback){ - this.call("svgCanvas.getSvgString()",callback) -} - -embedded_svg_edit.prototype.setSvgString = function(svg){ - this.call("svgCanvas.setSvgString('"+svg.replace(/'/g, "\\'")+"')"); -} -*/ - - -/* -Embedded SVG-edit API - -General usage: -- Have an iframe somewhere pointing to a version of svg-edit > r1000 -- Initialize the magic with: -var svgCanvas = new embedded_svg_edit(window.frames['svgedit']); -- Pass functions in this format: -svgCanvas.setSvgString("string") -- Or if a callback is needed: -svgCanvas.setSvgString("string")(function(data, error){ - if(error){ - //there was an error - }else{ - //handle data - } -}) - -Everything is done with the same API as the real svg-edit, -and all documentation is unchanged. The only difference is -when handling returns, the callback notation is used instead. - -var blah = new embedded_svg_edit(window.frames['svgedit']); -blah.clearSelection("woot","blah",1337,[1,2,3,4,5,"moo"],-42,{a: "tree",b:6, c: 9})(function(){console.log("GET DATA",arguments)}) -*/ - -function embedded_svg_edit(frame){ - //initialize communication - this.frame = frame; - //this.stack = [] //callback stack - this.callbacks = {}; //successor to stack - this.encode = embedded_svg_edit.encode; - //List of functions extracted with this: - //Run in firebug on http://svg-edit.googlecode.com/svn/trunk/docs/files/svgcanvas-js.html - - //for(var i=0,q=[],f = document.querySelectorAll("div.CFunction h3.CTitle a");i<\/style>').appendTo('head'); - } - conn_rules.text(!on?"":"#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }"); - $('#connector_panel').toggle(on); - } - - function setPoint(elem, pos, x, y, setMid) { - var pts = elem.points; - var pt = svgroot.createSVGPoint(); - pt.x = x; - pt.y = y; - if(pos === 'end') pos = pts.numberOfItems-1; - // TODO: Test for this on init, then use alt only if needed - try { - pts.replaceItem(pt, pos); - } catch(err) { - // Should only occur in FF which formats points attr as "n,n n,n", so just split - var pt_arr = elem.getAttribute("points").split(" "); - for(var i=0; i< pt_arr.length; i++) { - if(i == pos) { - pt_arr[i] = x + ',' + y; - } - } - elem.setAttribute("points",pt_arr.join(" ")); - } - - if(setMid) { - // Add center point - var pt_start = pts.getItem(0); - var pt_end = pts.getItem(pts.numberOfItems-1); - setPoint(elem, 1, (pt_end.x + pt_start.x)/2, (pt_end.y + pt_start.y)/2); - } - } - - function updateLine(diff_x, diff_y) { - // Update line with element - var i = connections.length; - while(i--) { - var conn = connections[i]; - var line = conn.connector; - var elem = conn.elem; - - var pre = conn.is_start?'start':'end'; -// var sw = line.getAttribute('stroke-width') * 5; - - // Update bbox for this element - var bb = elData(line, pre+'_bb'); - bb.x = conn.start_x + diff_x; - bb.y = conn.start_y + diff_y; - elData(line, pre+'_bb', bb); - - var alt_pre = conn.is_start?'end':'start'; - - // Get center pt of connected element - var bb2 = elData(line, alt_pre+'_bb'); - var src_x = bb2.x + bb2.width/2; - var src_y = bb2.y + bb2.height/2; - - // Set point of element being moved - var pt = getBBintersect(src_x, src_y, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0 - setPoint(line, conn.is_start?0:'end', pt.x, pt.y, true); - - // Set point of connected element - var pt2 = getBBintersect(pt.x, pt.y, elData(line, alt_pre + '_bb'), getOffset(alt_pre, line)); - setPoint(line, conn.is_start?'end':0, pt2.x, pt2.y, true); - - } - } - - function findConnectors(elems) { - if(!elems) elems = selElems; - var connectors = $(svgcontent).find(conn_sel); - connections = []; - - // Loop through connectors to see if one is connected to the element - connectors.each(function() { - var start = elData(this, "c_start"); - var end = elData(this, "c_end"); - - var parts = [getElem(start), getElem(end)]; - for(var i=0; i<2; i++) { - var c_elem = parts[i]; - var add_this = false; - // The connected element might be part of a selected group - $(c_elem).parents().each(function() { - if($.inArray(this, elems) !== -1) { - // Pretend this element is selected - add_this = true; - } - }); - - if(!c_elem || !c_elem.parentNode) { - $(this).remove(); - continue; - } - if($.inArray(c_elem, elems) !== -1 || add_this) { - var bb = svgCanvas.getStrokedBBox([c_elem]); - connections.push({ - elem: c_elem, - connector: this, - is_start: (i === 0), - start_x: bb.x, - start_y: bb.y - }); - } - } - }); - } - - function updateConnectors(elems) { - // Updates connector lines based on selected elements - // Is not used on mousemove, as it runs getStrokedBBox every time, - // which isn't necessary there. - findConnectors(elems); - if(connections.length) { - // Update line with element - var i = connections.length; - while(i--) { - var conn = connections[i]; - var line = conn.connector; - var elem = conn.elem; - - var sw = line.getAttribute('stroke-width') * 5; - var pre = conn.is_start?'start':'end'; - - // Update bbox for this element - var bb = svgCanvas.getStrokedBBox([elem]); - bb.x = conn.start_x; - bb.y = conn.start_y; - elData(line, pre+'_bb', bb); - var add_offset = elData(line, pre+'_off'); - - var alt_pre = conn.is_start?'end':'start'; - - // Get center pt of connected element - var bb2 = elData(line, alt_pre+'_bb'); - var src_x = bb2.x + bb2.width/2; - var src_y = bb2.y + bb2.height/2; - - // Set point of element being moved - var pt = getBBintersect(src_x, src_y, bb, getOffset(pre, line)); - setPoint(line, conn.is_start?0:'end', pt.x, pt.y, true); - - // Set point of connected element - var pt2 = getBBintersect(pt.x, pt.y, elData(line, alt_pre + '_bb'), getOffset(alt_pre, line)); - setPoint(line, conn.is_start?'end':0, pt2.x, pt2.y, true); - - // Update points attribute manually for webkit - if(navigator.userAgent.indexOf('AppleWebKit') != -1) { - var pts = line.points; - var len = pts.numberOfItems; - var pt_arr = Array(len); - for(var j=0; j< len; j++) { - var pt = pts.getItem(j); - pt_arr[j] = pt.x + ',' + pt.y; - } - line.setAttribute("points",pt_arr.join(" ")); - } - - } - } - } - - function getBBintersect(x, y, bb, offset) { - if(offset) { - offset -= 0; - bb = $.extend({}, bb); - bb.width += offset; - bb.height += offset; - bb.x -= offset/2; - bb.y -= offset/2; - } - - var mid_x = bb.x + bb.width/2; - var mid_y = bb.y + bb.height/2; - var len_x = x - mid_x; - var len_y = y - mid_y; - - var slope = Math.abs(len_y/len_x); - - var ratio; - - if(slope < bb.height/bb.width) { - ratio = (bb.width/2) / Math.abs(len_x); - } else { - ratio = (bb.height/2) / Math.abs(len_y); - } - - - return { - x: mid_x + len_x * ratio, - y: mid_y + len_y * ratio - } - } - - // Do once - (function() { - var gse = svgCanvas.groupSelectedElements; - - svgCanvas.groupSelectedElements = function() { - svgCanvas.removeFromSelection($(conn_sel).toArray()); - return gse.apply(this, arguments); - } - - var mse = svgCanvas.moveSelectedElements; - - svgCanvas.moveSelectedElements = function() { - svgCanvas.removeFromSelection($(conn_sel).toArray()); - var cmd = mse.apply(this, arguments); - updateConnectors(); - return cmd; - } - - se_ns = svgCanvas.getEditorNS(); - }()); - - // Do on reset - function init() { - // Make sure all connectors have data set - $(svgcontent).find('*').each(function() { - var conn = this.getAttributeNS(se_ns, "connector"); - if(conn) { - this.setAttribute('class', conn_sel.substr(1)); - var conn_data = conn.split(' '); - var sbb = svgCanvas.getStrokedBBox([getElem(conn_data[0])]); - var ebb = svgCanvas.getStrokedBBox([getElem(conn_data[1])]); - $(this).data('c_start',conn_data[0]) - .data('c_end',conn_data[1]) - .data('start_bb', sbb) - .data('end_bb', ebb); - svgCanvas.getEditorNS(true); - } - }); -// updateConnectors(); - } - -// $(svgroot).parent().mousemove(function(e) { -// // if(started -// // || svgCanvas.getMode() != "connector" -// // || e.target.parentNode.parentNode != svgcontent) return; -// -// console.log('y') -// // if(e.target.parentNode.parentNode === svgcontent) { -// // -// // } -// }); - - return { - name: "Connector", - svgicons: "images/conn.svg", - buttons: [{ - id: "mode_connect", - type: "mode", - icon: "images/cut.png", - title: "Connect two objects", - includeWith: { - button: '#tool_line', - isDefault: false, - position: 1 - }, - events: { - 'click': function() { - svgCanvas.setMode("connector"); - } - } - }], - addLangData: function(lang) { - return { - data: lang_list[lang] - }; - }, - mouseDown: function(opts) { - var e = opts.event; - start_x = opts.start_x, - start_y = opts.start_y; - var mode = svgCanvas.getMode(); - - if(mode == "connector") { - - if(started) return; - - var mouse_target = e.target; - - var parents = $(mouse_target).parents(); - - if($.inArray(svgcontent, parents) != -1) { - // Connectable element - - // If child of foreignObject, use parent - var fo = $(mouse_target).closest("foreignObject"); - start_elem = fo.length ? fo[0] : mouse_target; - - // Get center of source element - var bb = svgCanvas.getStrokedBBox([start_elem]); - var x = bb.x + bb.width/2; - var y = bb.y + bb.height/2; - - started = true; - cur_line = addElem({ - "element": "polyline", - "attr": { - "id": getNextId(), - "points": (x+','+y+' '+x+','+y+' '+start_x+','+start_y), - "stroke": '#' + curConfig.initStroke.color, - "stroke-width": (!start_elem.stroke_width || start_elem.stroke_width == 0) ? curConfig.initStroke.width : start_elem.stroke_width, - "fill": "none", - "opacity": curConfig.initStroke.opacity, - "style": "pointer-events:none" - } - }); - elData(cur_line, 'start_bb', bb); - } - return { - started: true - }; - } else if(mode == "select") { - findConnectors(); - } - }, - mouseMove: function(opts) { - var zoom = svgCanvas.getZoom(); - var e = opts.event; - var x = opts.mouse_x/zoom; - var y = opts.mouse_y/zoom; - - var diff_x = x - start_x, - diff_y = y - start_y; - - var mode = svgCanvas.getMode(); - - if(mode == "connector" && started) { - - var sw = cur_line.getAttribute('stroke-width') * 3; - // Set start point (adjusts based on bb) - var pt = getBBintersect(x, y, elData(cur_line, 'start_bb'), getOffset('start', cur_line)); - start_x = pt.x; - start_y = pt.y; - - setPoint(cur_line, 0, pt.x, pt.y, true); - - // Set end point - setPoint(cur_line, 'end', x, y, true); - } else if(mode == "select") { - var slen = selElems.length; - - while(slen--) { - var elem = selElems[slen]; - // Look for selected connector elements - if(elem && elData(elem, 'c_start')) { - // Remove the "translate" transform given to move - svgCanvas.removeFromSelection([elem]); - svgCanvas.getTransformList(elem).clear(); - - } - } - if(connections.length) { - updateLine(diff_x, diff_y); - - - } - } - }, - mouseUp: function(opts) { - var zoom = svgCanvas.getZoom(); - var e = opts.event, - x = opts.mouse_x/zoom, - y = opts.mouse_y/zoom, - mouse_target = e.target; - - if(svgCanvas.getMode() == "connector") { - var fo = $(mouse_target).closest("foreignObject"); - if(fo.length) mouse_target = fo[0]; - - var parents = $(mouse_target).parents(); - - if(mouse_target == start_elem) { - // Start line through click - started = true; - return { - keep: true, - element: null, - started: started - } - } else if($.inArray(svgcontent, parents) === -1) { - // Not a valid target element, so remove line - $(cur_line).remove(); - started = false; - return { - keep: false, - element: null, - started: started - } - } else { - // Valid end element - end_elem = mouse_target; - - var start_id = start_elem.id, end_id = end_elem.id; - var conn_str = start_id + " " + end_id; - var alt_str = end_id + " " + start_id; - // Don't create connector if one already exists - var dupe = $(svgcontent).find(conn_sel).filter(function() { - var conn = this.getAttributeNS(se_ns, "connector"); - if(conn == conn_str || conn == alt_str) return true; - }); - if(dupe.length) { - $(cur_line).remove(); - return { - keep: false, - element: null, - started: false - } - } - - var bb = svgCanvas.getStrokedBBox([end_elem]); - - var pt = getBBintersect(start_x, start_y, bb, getOffset('start', cur_line)); - setPoint(cur_line, 'end', pt.x, pt.y, true); - $(cur_line) - .data("c_start", start_id) - .data("c_end", end_id) - .data("end_bb", bb); - se_ns = svgCanvas.getEditorNS(true); - cur_line.setAttributeNS(se_ns, "se:connector", conn_str); - cur_line.setAttribute('class', conn_sel.substr(1)); - cur_line.setAttribute('opacity', 1); - svgCanvas.addToSelection([cur_line]); - svgCanvas.moveToBottomSelectedElement(); - selManager.requestSelector(cur_line).showGrips(false); - started = false; - return { - keep: true, - element: cur_line, - started: started - } - } - } - }, - selectedChanged: function(opts) { - // TODO: Find better way to skip operations if no connectors are in use - if(!$(svgcontent).find(conn_sel).length) return; - - if(svgCanvas.getMode() == 'connector') { - svgCanvas.setMode('select'); - } - - // Use this to update the current selected elements - selElems = opts.elems; - - var i = selElems.length; - - while(i--) { - var elem = selElems[i]; - if(elem && elData(elem, 'c_start')) { - selManager.requestSelector(elem).showGrips(false); - if(opts.selectedElement && !opts.multiselected) { - // TODO: Set up context tools and hide most regular line tools - showPanel(true); - } else { - showPanel(false); - } - } else { - showPanel(false); - } - } - updateConnectors(); - }, - elementChanged: function(opts) { - var elem = opts.elems[0]; - if (elem && elem.tagName == 'svg' && elem.id == "svgcontent") { - // Update svgcontent (can change on import) - svgcontent = elem; - init(); - } - - // Has marker, so change offset - if(elem && ( - elem.getAttribute("marker-start") || - elem.getAttribute("marker-mid") || - elem.getAttribute("marker-end") - )) { - var start = elem.getAttribute("marker-start"); - var mid = elem.getAttribute("marker-mid"); - var end = elem.getAttribute("marker-end"); - cur_line = elem; - $(elem) - .data("start_off", !!start) - .data("end_off", !!end); - - if(elem.tagName == "line" && mid) { - // Convert to polyline to accept mid-arrow - - var x1 = elem.getAttribute('x1')-0; - var x2 = elem.getAttribute('x2')-0; - var y1 = elem.getAttribute('y1')-0; - var y2 = elem.getAttribute('y2')-0; - var id = elem.id; - - var mid_pt = (' '+((x1+x2)/2)+','+((y1+y2)/2) + ' '); - var pline = addElem({ - "element": "polyline", - "attr": { - "points": (x1+','+y1+ mid_pt +x2+','+y2), - "stroke": elem.getAttribute('stroke'), - "stroke-width": elem.getAttribute('stroke-width'), - "marker-mid": mid, - "fill": "none", - "opacity": elem.getAttribute('opacity') || 1 - } - }); - $(elem).after(pline).remove(); - svgCanvas.clearSelection(); - pline.id = id; - svgCanvas.addToSelection([pline]); - elem = pline; - } - } - // Update line if it's a connector - if(elem.getAttribute('class') == conn_sel.substr(1)) { - var start = getElem(elData(elem, 'c_start')); - updateConnectors([start]); - } else { - updateConnectors(); - } - }, - toolButtonStateUpdate: function(opts) { - if(opts.nostroke) { - if ($('#mode_connect').hasClass('tool_button_current')) { - clickSelect(); - } - } - $('#mode_connect') - .toggleClass('disabled',opts.nostroke); - } - }; -}); diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/extensions/ext-eyedropper.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/extensions/ext-eyedropper.js deleted file mode 100644 index 06d12833..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/extensions/ext-eyedropper.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * ext-eyedropper.js - * - * Licensed under the MIT License - * - * Copyright(c) 2010 Jeff Schiller - * - */ - -// Dependencies: -// 1) jQuery -// 2) history.js -// 3) svg_editor.js -// 4) svgcanvas.js - -svgEditor.addExtension("eyedropper", function(S) { - var svgcontent = S.svgcontent, - svgns = "http://www.w3.org/2000/svg", - svgdoc = S.svgroot.parentNode.ownerDocument, - svgCanvas = svgEditor.canvas, - ChangeElementCommand = svgedit.history.ChangeElementCommand, - addToHistory = function(cmd) { svgCanvas.undoMgr.addCommandToHistory(cmd); }, - currentStyle = {fillPaint: "red", fillOpacity: 1.0, - strokePaint: "black", strokeOpacity: 1.0, - strokeWidth: 5, strokeDashArray: null, - opacity: 1.0, - strokeLinecap: 'butt', - strokeLinejoin: 'miter', - }; - - function getStyle(opts) { - // if we are in eyedropper mode, we don't want to disable the eye-dropper tool - var mode = svgCanvas.getMode(); - if (mode == "eyedropper") return; - - var elem = null; - var tool = $('#tool_eyedropper'); - // enable-eye-dropper if one element is selected - if (!opts.multiselected && opts.elems[0] && - $.inArray(opts.elems[0].nodeName, ['svg', 'g', 'use']) == -1) - { - elem = opts.elems[0]; - tool.removeClass('disabled'); - // grab the current style - currentStyle.fillPaint = elem.getAttribute("fill") || "black"; - currentStyle.fillOpacity = elem.getAttribute("fill-opacity") || 1.0; - currentStyle.strokePaint = elem.getAttribute("stroke"); - currentStyle.strokeOpacity = elem.getAttribute("stroke-opacity") || 1.0; - currentStyle.strokeWidth = elem.getAttribute("stroke-width"); - currentStyle.strokeDashArray = elem.getAttribute("stroke-dasharray"); - currentStyle.strokeLinecap = elem.getAttribute("stroke-linecap"); - currentStyle.strokeLinejoin = elem.getAttribute("stroke-linejoin"); - currentStyle.opacity = elem.getAttribute("opacity") || 1.0; - } - // disable eye-dropper tool - else { - tool.addClass('disabled'); - } - - } - - return { - name: "eyedropper", - svgicons: "extensions/eyedropper-icon.xml", - buttons: [{ - id: "tool_eyedropper", - type: "mode", - title: "Eye Dropper Tool", - key: "I", - events: { - "click": function() { - svgCanvas.setMode("eyedropper"); - } - } - }], - - // if we have selected an element, grab its paint and enable the eye dropper button - selectedChanged: getStyle, - elementChanged: getStyle, - - mouseDown: function(opts) { - var mode = svgCanvas.getMode(); - if (mode == "eyedropper") { - var e = opts.event; - var target = e.target; - if ($.inArray(target.nodeName, ['svg', 'g', 'use']) == -1) { - var changes = {}; - - var change = function(elem, attrname, newvalue) { - changes[attrname] = elem.getAttribute(attrname); - elem.setAttribute(attrname, newvalue); - }; - - if (currentStyle.fillPaint) change(target, "fill", currentStyle.fillPaint); - if (currentStyle.fillOpacity) change(target, "fill-opacity", currentStyle.fillOpacity); - if (currentStyle.strokePaint) change(target, "stroke", currentStyle.strokePaint); - if (currentStyle.strokeOpacity) change(target, "stroke-opacity", currentStyle.strokeOpacity); - if (currentStyle.strokeWidth) change(target, "stroke-width", currentStyle.strokeWidth); - if (currentStyle.strokeDashArray) change(target, "stroke-dasharray", currentStyle.strokeDashArray); - if (currentStyle.opacity) change(target, "opacity", currentStyle.opacity); - if (currentStyle.strokeLinecap) change(target, "stroke-linecap", currentStyle.strokeLinecap); - if (currentStyle.strokeLinejoin) change(target, "stroke-linejoin", currentStyle.strokeLinejoin); - - addToHistory(new ChangeElementCommand(target, changes)); - } - } - }, - }; -}); diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/extensions/ext-foreignobject.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/extensions/ext-foreignobject.js deleted file mode 100644 index 9d401825..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/extensions/ext-foreignobject.js +++ /dev/null @@ -1,277 +0,0 @@ -/* - * ext-foreignobject.js - * - * Licensed under the Apache License, Version 2 - * - * Copyright(c) 2010 Jacques Distler - * Copyright(c) 2010 Alexis Deveria - * - */ - -svgEditor.addExtension("foreignObject", function(S) { - var svgcontent = S.svgcontent, - addElem = S.addSvgElementFromJson, - selElems, - svgns = "http://www.w3.org/2000/svg", - xlinkns = "http://www.w3.org/1999/xlink", - xmlns = "http://www.w3.org/XML/1998/namespace", - xmlnsns = "http://www.w3.org/2000/xmlns/", - se_ns = "http://svg-edit.googlecode.com", - htmlns = "http://www.w3.org/1999/xhtml", - mathns = "http://www.w3.org/1998/Math/MathML", - editingforeign = false, - svgdoc = S.svgroot.parentNode.ownerDocument, - started, - newFO; - - - var properlySourceSizeTextArea = function(){ - // TODO: remove magic numbers here and get values from CSS - var height = $('#svg_source_container').height() - 80; - $('#svg_source_textarea').css('height', height); - }; - - function showPanel(on) { - var fc_rules = $('#fc_rules'); - if(!fc_rules.length) { - fc_rules = $(' -
    - -
    - - -
    -
    -

    Layers

    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - -
    Layer 1
    - Move elements to: - -
    -
    L a y e r s
    -
    - -
    -
    - SVG-Edit - - -
    - - -
    - - - -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - -
    - - - -
    - - -
    - - - -
    - - -
    -
    - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - -
    -
    - - -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -
    B
    -
    i
    -
    - -
    - - -
    - - - - - -
    - - -
    -
    - - - - -
    - -
    - -
    - -
    -
    -
    - - -
    - -
    - -
    -
    - -
    - - - - -
    -
    -
    -
    -
    -
    -
    - -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - - -
    - - -
    -
    - -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - - - -
    - - - - - - - -
    - -
    - - -
    -
    - -
    - -
    -
    -
    - -
    - - - - - -
    - - - -
    -
    -
    -
    - - -
    -
    -

    Copy the contents of this box into a text editor, then save the file with a .svg extension.

    - -
    -
    - -
    -
    -
    - - -
    -
    -
    -
    - - -
    - - -
    - Image Properties - - -
    - Canvas Dimensions - - - - - - -
    - -
    - Included Images - - -
    -
    - -
    -
    - -
    -
    -
    -
    - - -
    - -
    - Editor Preferences - - - - - -
    - Editor Background -
    - -

    Note: Background will not be saved with image.

    -
    - -
    - Grid - - -
    - -
    - Units & Rulers - - - - -
    - -
    - -
    -
    - -
    -
    -
    -
    -
    -
    -
    - - - - - - - - - diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svg-editor.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svg-editor.js deleted file mode 100644 index 55a06a66..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svg-editor.js +++ /dev/null @@ -1,4988 +0,0 @@ -/* - * svg-editor.js - * - * Licensed under the MIT License - * - * Copyright(c) 2010 Alexis Deveria - * Copyright(c) 2010 Pavol Rusnak - * Copyright(c) 2010 Jeff Schiller - * Copyright(c) 2010 Narendra Sisodiya - * Copyright(c) 2016 Glutanimate - * - */ - -// Dependencies: -// 1) units.js -// 2) browser.js -// 3) svgcanvas.js - -// This version of svg-edit has been adapted to work with Image Occlusion Enhanced - -// Modifications not specifically documented in commits: -// - merge magnebra/add_text_config_options -// -// Please consult the commit log for all other changes to this file - -(function() { - - document.addEventListener("touchstart", touchHandler, true); - document.addEventListener("touchmove", touchHandler, true); - document.addEventListener("touchend", touchHandler, true); - document.addEventListener("touchcancel", touchHandler, true); - - if(!window.svgEditor) window.svgEditor = function($) { - var svgCanvas; - var Editor = {}; - var is_ready = false; - - var defaultPrefs = { - lang:'en', - iconsize:'m', - bkgd_color:'#FFF', - bkgd_url:'', - img_save:'embed' - }, - curPrefs = {}, - - // Note: Difference between Prefs and Config is that Prefs can be - // changed in the UI and are stored in the browser, config can not - - curConfig = { - canvasName: 'default', - canvas_expansion: 3, - dimensions: [640,480], - initFill: { - color: 'FF0000', // solid red - opacity: 1 - }, - initStroke: { - width: 5, - color: '000000', // solid black - opacity: 1 - }, - text: { - stroke_width: 0, - font_size: 24, - font_family: 'serif' - }, - initOpacity: 1, - imgPath: 'images/', - langPath: 'locale/', - extPath: 'extensions/', - jGraduatePath: 'jgraduate/images/', - extensions: ['ext-markers.js','ext-connector.js', 'ext-eyedropper.js', 'ext-shapes.js', 'ext-imagelib.js','ext-grid.js'], - initTool: 'select', - wireframe: false, - colorPickerCSS: null, - gridSnapping: false, - gridColor: "#000", - baseUnit: 'px', - snappingStep: 10, - showRulers: true - }, - uiStrings = Editor.uiStrings = { - common: { - "ok":"OK", - "cancel":"Cancel", - "key_up":"Up", - "key_down":"Down", - "key_backspace":"Backspace", - "key_del":"Del" - - }, - // This is needed if the locale is English, since the locale strings are not read in that instance. - layers: { - "layer":"Layer" - }, - notification: { - "invalidAttrValGiven":"Invalid value given", - "noContentToFitTo":"No content to fit to", - "dupeLayerName":"There is already a layer named that!", - "enterUniqueLayerName":"Please enter a unique layer name", - "enterNewLayerName":"Please enter the new layer name", - "layerHasThatName":"Layer already has that name", - "QmoveElemsToLayer":"Move selected elements to layer \"%s\"?", - "QwantToClear":"Do you want to clear the drawing?\nThis will also erase your undo history!", - "QwantToOpen":"Do you want to open a new file?\nThis will also erase your undo history!", - "QerrorsRevertToSource":"There were parsing errors in your SVG source.\nRevert back to original SVG source?", - "QignoreSourceChanges":"Ignore changes made to SVG source?", - "featNotSupported":"Feature not supported", - "enterNewImgURL":"Enter the new image URL", - "defsFailOnSave": "NOTE: Due to a bug in your browser, this image may appear wrong (missing gradients or elements). It will however appear correct once actually saved.", - "loadingImage":"Loading image, please wait...", - "saveFromBrowser": "Select \"Save As...\" in your browser to save this image as a %s file.", - "noteTheseIssues": "Also note the following issues: ", - "unsavedChanges": "There are unsaved changes.", - "enterNewLinkURL": "Enter the new hyperlink URL", - "errorLoadingSVG": "Error: Unable to load SVG data", - "URLloadFail": "Unable to load from URL", - "retrieving": 'Retrieving "%s" ...' - } - }; - - var curPrefs = {}; //$.extend({}, defaultPrefs); - - var customHandlers = {}; - - Editor.curConfig = curConfig; - - Editor.tool_scale = 1; - - // Store and retrieve preferences - $.pref = function(key, val) { - if(val) curPrefs[key] = val; - key = 'svg-edit-'+key; - var host = location.hostname, - onweb = host && host.indexOf('.') >= 0, - store = (val != undefined), - storage = false; - // Some FF versions throw security errors here - try { - if(window.localStorage) { // && onweb removed so Webkit works locally - storage = localStorage; - } - } catch(e) {} - try { - if(window.globalStorage && onweb) { - storage = globalStorage[host]; - } - } catch(e) {} - - if(storage) { - if(store) storage.setItem(key, val); - else if (storage.getItem(key)) return storage.getItem(key) + ''; // Convert to string for FF (.value fails in Webkit) - } else if(window.widget) { - if(store) widget.setPreferenceForKey(val, key); - else return widget.preferenceForKey(key); - } else { - if(store) { - var d = new Date(); - d.setTime(d.getTime() + 31536000000); - val = encodeURIComponent(val); - document.cookie = key+'='+val+'; expires='+d.toUTCString(); - } else { - var result = document.cookie.match(new RegExp(key + "=([^;]+)")); - return result?decodeURIComponent(result[1]):''; - } - } - } - - Editor.setConfig = function(opts) { - $.each(opts, function(key, val) { - // Only allow prefs defined in defaultPrefs - if(key in defaultPrefs) { - $.pref(key, val); - } - }); - $.extend(true, curConfig, opts); - if(opts.extensions) { - curConfig.extensions = opts.extensions; - } - - } - - // Extension mechanisms must call setCustomHandlers with two functions: opts.open and opts.save - // opts.open's responsibilities are: - // - invoke a file chooser dialog in 'open' mode - // - let user pick a SVG file - // - calls setCanvas.setSvgString() with the string contents of that file - // opts.save's responsibilities are: - // - accept the string contents of the current document - // - invoke a file chooser dialog in 'save' mode - // - save the file to location chosen by the user - Editor.setCustomHandlers = function(opts) { - Editor.ready(function() { - if(opts.open) { - $('#tool_open > input[type="file"]').remove(); - $('#tool_open').show(); - svgCanvas.open = opts.open; - } - if(opts.save) { - Editor.show_save_warning = false; - svgCanvas.bind("saved", opts.save); - } - if(opts.pngsave) { - svgCanvas.bind("exported", opts.pngsave); - } - customHandlers = opts; - }); - } - - Editor.randomizeIds = function() { - svgCanvas.randomizeIds(arguments) - } - - Editor.init = function() { - // For external openers - (function() { - // let the opener know SVG Edit is ready - var w = window.opener; - if (w) { - try { - var svgEditorReadyEvent = w.document.createEvent("Event"); - svgEditorReadyEvent.initEvent("svgEditorReady", true, true); - w.document.documentElement.dispatchEvent(svgEditorReadyEvent); - } - catch(e) {} - } - })(); - - (function() { - // Load config/data from URL if given - var urldata = $.deparam.querystring(true); - if(!$.isEmptyObject(urldata)) { - if(urldata.dimensions) { - urldata.dimensions = urldata.dimensions.split(','); - } - - if(urldata.extensions) { - urldata.extensions = urldata.extensions.split(','); - } - - if(urldata.bkgd_color) { - urldata.bkgd_color = '#' + urldata.bkgd_color; - } - - svgEditor.setConfig(urldata); - - var src = urldata.source; - var qstr = $.param.querystring(); - - if(!src) { // urldata.source may have been null if it ended with '=' - if(qstr.indexOf('source=data:') >= 0) { - src = qstr.match(/source=(data:[^&]*)/)[1]; - } - } - - if(src) { - if(src.indexOf("data:") === 0) { - // plusses get replaced by spaces, so re-insert - src = src.replace(/ /g, "+"); - Editor.loadFromDataURI(src); - } else { - Editor.loadFromString(src); - } - } else if(qstr.indexOf('paramurl=') !== -1) { - // Get paramater URL (use full length of remaining location.href) - svgEditor.loadFromURL(qstr.substr(9)); - } else if(urldata.url) { - svgEditor.loadFromURL(urldata.url); - } - } else { - var name = 'svgedit-' + Editor.curConfig.canvasName; - var cached = window.localStorage.getItem(name); - if (cached) { - Editor.loadFromString(cached); - } - } - })(); - - var extFunc = function() { - $.each(curConfig.extensions, function() { - var extname = this; - $.getScript(curConfig.extPath + extname, function(d) { - // Fails locally in Chrome 5 - if(!d) { - var s = document.createElement('script'); - s.src = curConfig.extPath + extname; - document.querySelector('head').appendChild(s); - } - }); - }); - - var good_langs = []; - - $('#lang_select option').each(function() { - good_langs.push(this.value); - }); - - // var lang = ('lang' in curPrefs) ? curPrefs.lang : null; - Editor.putLocale(null, good_langs); - } - - // Load extensions - // Bit of a hack to run extensions in local Opera/IE9 - if(document.location.protocol === 'file:') { - setTimeout(extFunc, 100); - } else { - extFunc(); - } - $.svgIcons(curConfig.imgPath + 'svg_edit_icons.svg', { - w:24, h:24, - id_match: false, - no_img: !svgedit.browser.isWebkit(), // Opera & Firefox 4 gives odd behavior w/images - fallback_path: curConfig.imgPath, - fallback:{ - 'new_image':'clear.png', - 'save':'save.png', - 'open':'open.png', - 'source':'source.png', - 'docprops':'document-properties.png', - 'wireframe':'wireframe.png', - - 'undo':'undo.png', - 'redo':'redo.png', - - 'select':'select.png', - 'select_node':'select_node.png', - 'pencil':'fhpath.png', - 'pen':'line.png', - 'square':'square.png', - 'rect':'rect.png', - 'fh_rect':'freehand-square.png', - 'circle':'circle.png', - 'ellipse':'ellipse.png', - 'fh_ellipse':'freehand-circle.png', - 'path':'path.png', - 'text':'text.png', - 'image':'image.png', - 'zoom':'zoom.png', - - 'clone':'clone.png', - 'node_clone':'node_clone.png', - 'delete':'delete.png', - 'node_delete':'node_delete.png', - 'group':'shape_group.png', - 'ungroup':'shape_ungroup.png', - 'move_top':'move_top.png', - 'move_bottom':'move_bottom.png', - 'to_path':'to_path.png', - 'link_controls':'link_controls.png', - 'reorient':'reorient.png', - - 'align_left':'align-left.png', - 'align_center':'align-center', - 'align_right':'align-right', - 'align_top':'align-top', - 'align_middle':'align-middle', - 'align_bottom':'align-bottom', - - 'go_up':'go-up.png', - 'go_down':'go-down.png', - - 'ok':'save.png', - 'cancel':'cancel.png', - - 'arrow_right':'flyouth.png', - 'arrow_down':'dropdown.gif' - }, - placement: { - '#logo':'logo', - - '#tool_clear div,#layer_new':'new_image', - '#tool_save div':'save', - '#tool_export div':'export', - '#tool_open div div':'open', - '#tool_import div div':'import', - '#tool_source':'source', - '#tool_docprops > div':'docprops', - '#tool_wireframe':'wireframe', - - '#tool_undo':'undo', - '#tool_redo':'redo', - - '#tool_select':'select', - '#tool_fhpath':'pencil', - '#tool_line':'pen', - '#tool_rect,#tools_rect_show':'rect', - '#tool_square':'square', - '#tool_fhrect':'fh_rect', - '#tool_ellipse,#tools_ellipse_show':'ellipse', - '#tool_circle':'circle', - '#tool_fhellipse':'fh_ellipse', - '#tool_path':'path', - '#tool_text,#layer_rename':'text', - '#tool_image':'image', - '#tool_zoom':'zoom', - - '#tool_clone,#tool_clone_multi':'clone', - '#tool_node_clone':'node_clone', - '#layer_delete,#tool_delete,#tool_delete_multi':'delete', - '#tool_node_delete':'node_delete', - '#tool_add_subpath':'add_subpath', - '#tool_openclose_path':'open_path', - '#tool_move_top':'move_top', - '#tool_move_bottom':'move_bottom', - '#tool_topath':'to_path', - '#tool_node_link':'link_controls', - '#tool_reorient':'reorient', - '#tool_group':'group', - '#tool_ungroup':'ungroup', - '#tool_unlink_use':'unlink_use', - - '#tool_alignleft, #tool_posleft':'align_left', - '#tool_aligncenter, #tool_poscenter':'align_center', - '#tool_alignright, #tool_posright':'align_right', - '#tool_aligntop, #tool_postop':'align_top', - '#tool_alignmiddle, #tool_posmiddle':'align_middle', - '#tool_alignbottom, #tool_posbottom':'align_bottom', - '#cur_position':'align', - - '#linecap_butt,#cur_linecap':'linecap_butt', - '#linecap_round':'linecap_round', - '#linecap_square':'linecap_square', - - '#linejoin_miter,#cur_linejoin':'linejoin_miter', - '#linejoin_round':'linejoin_round', - '#linejoin_bevel':'linejoin_bevel', - - '#url_notice':'warning', - - '#layer_up':'go_up', - '#layer_down':'go_down', - '#layer_moreopts':'context_menu', - '#layerlist td.layervis':'eye', - - '#tool_source_save,#tool_docprops_save,#tool_prefs_save':'ok', - '#tool_source_cancel,#tool_docprops_cancel,#tool_prefs_cancel':'cancel', - - '#rwidthLabel, #iwidthLabel':'width', - '#rheightLabel, #iheightLabel':'height', - '#cornerRadiusLabel span':'c_radius', - '#angleLabel':'angle', - '#linkLabel,#tool_make_link,#tool_make_link_multi':'globe_link', - '#zoomLabel':'zoom', - '#tool_fill label': 'fill', - '#tool_stroke .icon_label': 'stroke', - '#group_opacityLabel': 'opacity', - '#blurLabel': 'blur', - '#font_sizeLabel': 'fontsize', - - '.flyout_arrow_horiz':'arrow_right', - '.dropdown button, #main_button .dropdown':'arrow_down', - '#palette .palette_item:first, #fill_bg, #stroke_bg':'no_color' - }, - resize: { - '#logo .svg_icon': 28, - '.flyout_arrow_horiz .svg_icon': 5, - '.layer_button .svg_icon, #layerlist td.layervis .svg_icon': 14, - '.dropdown button .svg_icon': 7, - '#main_button .dropdown .svg_icon': 9, - '.palette_item:first .svg_icon' : 15, - '#fill_bg .svg_icon, #stroke_bg .svg_icon': 16, - '.toolbar_button button .svg_icon':16, - '.stroke_tool div div .svg_icon': 20, - '#tools_bottom label .svg_icon': 18 - }, - callback: function(icons) { - $('.toolbar_button button > svg, .toolbar_button button > img').each(function() { - $(this).parent().prepend(this); - }); - - var tleft = $('#tools_left'); - if (tleft.length != 0) { - var min_height = tleft.offset().top + tleft.outerHeight(); - } -// var size = $.pref('iconsize'); -// if(size && size != 'm') { -// svgEditor.setIconSize(size); -// } else if($(window).height() < min_height) { -// // Make smaller -// svgEditor.setIconSize('s'); -// } - - // Look for any missing flyout icons from plugins - $('.tools_flyout').each(function() { - var shower = $('#' + this.id + '_show'); - var sel = shower.attr('data-curopt'); - // Check if there's an icon here - if(!shower.children('svg, img').length) { - var clone = $(sel).children().clone(); - if(clone.length) { - clone[0].removeAttribute('style'); //Needed for Opera - shower.append(clone); - } - } - }); - - svgEditor.runCallbacks(); - - setTimeout(function() { - $('.flyout_arrow_horiz:empty').each(function() { - $(this).append($.getSvgIcon('arrow_right').width(5).height(5)); - }); - }, 1); - } - }); - - Editor.canvas = svgCanvas = new $.SvgCanvas(document.getElementById("svgcanvas"), curConfig); - Editor.show_save_warning = false; - var palette = [ "#" + curConfig.initFill["color"], "#000000", "#3f3f3f", "#7f7f7f", "#bfbfbf", "#ffffff", - "#ff0000", "#ff7f00", "#ffff00", "#7fff00", - "#00ff00", "#00ff7f", "#00ffff", "#007fff", - "#0000ff", "#7f00ff", "#ff00ff", "#ff007f", - "#7f0000", "#7f3f00", "#7f7f00", "#3f7f00", - "#007f00", "#007f3f", "#007f7f", "#003f7f", - "#00007f", "#3f007f", "#7f007f", "#7f003f", - "#ffaaaa", "#ffd4aa", "#ffffaa", "#d4ffaa", - "#aaffaa", "#aaffd4", "#aaffff", "#aad4ff", - "#aaaaff", "#d4aaff", "#ffaaff", "#ffaad4" - ], - isMac = (navigator.platform.indexOf("Mac") >= 0), - isWebkit = (navigator.userAgent.indexOf("AppleWebKit") >= 0), - modKey = (isMac ? "meta+" : "ctrl+"), // ⌘ - path = svgCanvas.pathActions, - undoMgr = svgCanvas.undoMgr, - Utils = svgedit.utilities, - default_img_url = curConfig.imgPath + "logo.png", - workarea = $("#workarea"), - canv_menu = $("#cmenu_canvas"), - layer_menu = $("#cmenu_layers"), - exportWindow = null, - tool_scale = 1, - zoomInIcon = 'crosshair', - zoomOutIcon = 'crosshair', - ui_context = 'toolbars', - orig_source = '', - paintBox = {fill: null, stroke:null}; - - // This sets up alternative dialog boxes. They mostly work the same way as - // their UI counterparts, expect instead of returning the result, a callback - // needs to be included that returns the result as its first parameter. - // In the future we may want to add additional types of dialog boxes, since - // they should be easy to handle this way. - (function() { - $('#dialog_container').draggable({cancel:'#dialog_content, #dialog_buttons *', containment: 'window'}); - var box = $('#dialog_box'), btn_holder = $('#dialog_buttons'); - - var dbox = function(type, msg, callback, defText) { - $('#dialog_content').html('

    '+msg.replace(/\n/g,'

    ')+'

    ') - .toggleClass('prompt',(type=='prompt')); - btn_holder.empty(); - - var ok = $('').appendTo(btn_holder); - - if(type != 'alert') { - $('') - .appendTo(btn_holder) - .click(function() { box.hide();callback(false)}); - } - - if(type == 'prompt') { - var input = $('').prependTo(btn_holder); - input.val(defText || ''); - input.bind('keydown', 'return', function() {ok.click();}); - } - - if(type == 'process') { - ok.hide(); - } - - box.show(); - - ok.click(function() { - box.hide(); - var resp = (type == 'prompt')?input.val():true; - if(callback) callback(resp); - }).focus(); - - if(type == 'prompt') input.focus(); - } - - $.alert = function(msg, cb) { dbox('alert', msg, cb);}; - $.confirm = function(msg, cb) { dbox('confirm', msg, cb);}; - $.process_cancel = function(msg, cb) { dbox('process', msg, cb);}; - $.prompt = function(msg, txt, cb) { dbox('prompt', msg, cb, txt);}; - }()); - - var setSelectMode = function() { - var curr = $('.tool_button_current'); - if(curr.length && curr[0].id !== 'tool_select') { - curr.removeClass('tool_button_current').addClass('tool_button'); - $('#tool_select').addClass('tool_button_current').removeClass('tool_button'); - $('#styleoverrides').text('#svgcanvas svg *{cursor:move;pointer-events:all} #svgcanvas svg{cursor:default}'); - } - svgCanvas.setMode('select'); - workarea.css('cursor','auto'); - }; - - var togglePathEditMode = function(editmode, elems) { - $('#path_node_panel').toggle(editmode); - $('#tools_bottom_2,#tools_bottom_3').toggle(!editmode); - if(editmode) { - // Change select icon - $('.tool_button_current').removeClass('tool_button_current').addClass('tool_button'); - $('#tool_select').addClass('tool_button_current').removeClass('tool_button'); - setIcon('#tool_select', 'select_node'); - multiselected = false; - if(elems.length) { - selectedElement = elems[0]; - } - } else { - setIcon('#tool_select', 'select'); - } - } - - // used to make the flyouts stay on the screen longer the very first time - var flyoutspeed = 1250; - var textBeingEntered = false; - var selectedElement = null; - var multiselected = false; - var editingsource = false; - var docprops = false; - var preferences = false; - var cur_context = ''; - var orig_title = $('title:first').text(); - - var saveHandler = function(window,svg) { - Editor.show_save_warning = false; - - // by default, we add the XML prolog back, systems integrating SVG-edit (wikis, CMSs) - // can just provide their own custom save handler and might not want the XML prolog - svg = '\n' + svg; - - // Opens the SVG in new window, with warning about Mozilla bug #308590 when applicable - - var ua = navigator.userAgent; - - // Chrome 5 (and 6?) don't allow saving, show source instead ( http://code.google.com/p/chromium/issues/detail?id=46735 ) - // IE9 doesn't allow standalone Data URLs ( https://connect.microsoft.com/IE/feedback/details/542600/data-uri-images-fail-when-loaded-by-themselves ) - if((~ua.indexOf('Chrome') && $.browser.version >= 533) || ~ua.indexOf('MSIE')) { - showSourceEditor(0,true); - return; - } - var win = window.open("data:image/svg+xml;base64," + Utils.encode64(svg)); - - // Alert will only appear the first time saved OR the first time the bug is encountered - var done = $.pref('save_notice_done'); - if(done !== "all") { - - var note = uiStrings.notification.saveFromBrowser.replace('%s', 'SVG'); - - // Check if FF and has - if(ua.indexOf('Gecko/') !== -1) { - if(svg.indexOf('', {id: 'export_canvas'}).hide().appendTo('body'); - } - var c = $('#export_canvas')[0]; - - c.width = svgCanvas.contentW; - c.height = svgCanvas.contentH; - canvg(c, data.svg, {renderCallback: function() { - var datauri = c.toDataURL('image/png'); - exportWindow.location.href = datauri; - var done = $.pref('export_notice_done'); - if(done !== "all") { - var note = uiStrings.notification.saveFromBrowser.replace('%s', 'PNG'); - - // Check if there's issues - if(issues.length) { - var pre = "\n \u2022 "; - note += ("\n\n" + uiStrings.notification.noteTheseIssues + pre + issues.join(pre)); - } - - // Note that this will also prevent the notice even though new issues may appear later. - // May want to find a way to deal with that without annoying the user - $.pref('export_notice_done', 'all'); - exportWindow.alert(note); - } - }}); - }; - - // called when we've selected a different element - var selectedChanged = function(window,elems) { - var mode = svgCanvas.getMode(); - if(mode === "select") setSelectMode(); - var is_node = (mode == "pathedit"); - // if elems[1] is present, then we have more than one element - selectedElement = (elems.length == 1 || elems[1] == null ? elems[0] : null); - multiselected = (elems.length >= 2 && elems[1] != null); - if (selectedElement != null) { - // unless we're already in always set the mode of the editor to select because - // upon creation of a text element the editor is switched into - // select mode and this event fires - we need our UI to be in sync - - if (!is_node) { - updateToolbar(); - } - - } // if (elem != null) - - // Deal with pathedit mode - togglePathEditMode(is_node, elems); - updateContextPanel(); - svgCanvas.runExtensions("selectedChanged", { - elems: elems, - selectedElement: selectedElement, - multiselected: multiselected - }); - }; - - // Call when part of element is in process of changing, generally - // on mousemove actions like rotate, move, etc. - var elementTransition = function(window,elems) { - var mode = svgCanvas.getMode(); - var elem = elems[0]; - - if(!elem) return; - - multiselected = (elems.length >= 2 && elems[1] != null); - // Only updating fields for single elements for now - if(!multiselected) { - switch ( mode ) { - case "rotate": - var ang = svgCanvas.getRotationAngle(elem); - $('#angle').val(ang); - $('#tool_reorient').toggleClass('disabled', ang == 0); - break; - - // TODO: Update values that change on move/resize, etc -// case "select": -// case "resize": -// break; - } - } - svgCanvas.runExtensions("elementTransition", { - elems: elems - }); - }; - - // called when any element has changed - var elementChanged = function(window,elems) { - var mode = svgCanvas.getMode(); - if(mode === "select") { - setSelectMode(); - } - - for (var i = 0; i < elems.length; ++i) { - var elem = elems[i]; - - // if the element changed was the svg, then it could be a resolution change - if (elem && elem.tagName === "svg") { - populateLayers(); - updateCanvas(); - } - // Update selectedElement if element is no longer part of the image. - // This occurs for the text elements in Firefox - else if(elem && selectedElement && selectedElement.parentNode == null) { -// || elem && elem.tagName == "path" && !multiselected) { // This was added in r1430, but not sure why - selectedElement = elem; - } - } - - Editor.show_save_warning = true; - - // we update the contextual panel with potentially new - // positional/sizing information (we DON'T want to update the - // toolbar here as that creates an infinite loop) - // also this updates the history buttons - - // we tell it to skip focusing the text control if the - // text element was previously in focus - updateContextPanel(); - - // In the event a gradient was flipped: - if(selectedElement && mode === "select") { - paintBox.fill.update(); - paintBox.stroke.update(); - } - - svgCanvas.runExtensions("elementChanged", { - elems: elems - }); - }; - - var zoomChanged = svgCanvas.zoomChanged = function(window, bbox, autoCenter) { - var scrbar = 15, - res = svgCanvas.getResolution(), - w_area = workarea, - canvas_pos = $('#svgcanvas').position(); - var z_info = svgCanvas.setBBoxZoom(bbox, w_area.width()-scrbar, w_area.height()-scrbar); - if(!z_info) return; - var zoomlevel = z_info.zoom, - bb = z_info.bbox; - - if(zoomlevel < .001) { - changeZoom({value: .1}); - return; - } - -// $('#zoom').val(Math.round(zoomlevel*100)); - $('#zoom').val(zoomlevel*100); - - if(autoCenter) { - updateCanvas(); - } else { - updateCanvas(false, {x: bb.x * zoomlevel + (bb.width * zoomlevel)/2, y: bb.y * zoomlevel + (bb.height * zoomlevel)/2}); - } - - if(svgCanvas.getMode() == 'zoom' && bb.width) { - // Go to select if a zoom box was drawn - setSelectMode(); - } - - zoomDone(); - } - - - $('#cur_context_panel').delegate('a', 'click', function() { - var link = $(this); - if(link.attr('data-root')) { - svgCanvas.leaveContext(); - } else { - svgCanvas.setContext(link.text()); - } - svgCanvas.clearSelection(); - return false; - }); - - var contextChanged = function(win, context) { - var link_str = ''; - if(context) { - var str = ''; - link_str = '' + svgCanvas.getCurrentDrawing().getCurrentLayerName() + ''; - - $(context).parentsUntil('#svgcontent > g').andSelf().each(function() { - if(this.id) { - str += ' > ' + this.id; - if(this !== context) { - link_str += ' > ' + this.id + ''; - } else { - link_str += ' > ' + this.id; - } - } - }); - - cur_context = str; - } else { - cur_context = null; - } - $('#cur_context_panel').toggle(!!context).html(link_str); - - - updateTitle(); - } - - // Makes sure the current selected paint is available to work with - var prepPaints = function() { - paintBox.fill.prep(); - paintBox.stroke.prep(); - } - - var flyout_funcs = {}; - - var setupFlyouts = function(holders) { - $.each(holders, function(hold_sel, btn_opts) { - var buttons = $(hold_sel).children(); - var show_sel = hold_sel + '_show'; - var shower = $(show_sel); - var def = false; - buttons.addClass('tool_button') - .unbind('click mousedown mouseup') // may not be necessary - .each(function(i) { - // Get this buttons options - var opts = btn_opts[i]; - - // Remember the function that goes with this ID - flyout_funcs[opts.sel] = opts.fn; - - if(opts.isDefault) def = i; - - // Clicking the icon in flyout should set this set's icon - var func = function(event) { - var options = opts; - //find the currently selected tool if comes from keystroke - if (event.type === "keydown") { - var flyoutIsSelected = $(options.parent + "_show").hasClass('tool_button_current'); - var currentOperation = $(options.parent + "_show").attr("data-curopt"); - $.each(holders[opts.parent], function(i, tool){ - if (tool.sel == currentOperation) { - if(!event.shiftKey || !flyoutIsSelected) { - options = tool; - } - else { - options = holders[opts.parent][i+1] || holders[opts.parent][0]; - } - } - }); - } - if($(this).hasClass('disabled')) return false; - if (toolButtonClick(show_sel)) { - options.fn(); - } - if(options.icon) { - var icon = $.getSvgIcon(options.icon, true); - } else { - var icon = $(options.sel).children().eq(0).clone(); - } - - icon[0].setAttribute('width',shower.width()); - icon[0].setAttribute('height',shower.height()); - shower.children(':not(.flyout_arrow_horiz)').remove(); - shower.append(icon).attr('data-curopt', options.sel); // This sets the current mode - } - - $(this).mouseup(func); - - if(opts.key) { - $(document).bind('keydown', opts.key[0] + " shift+" + opts.key[0], func); - } - }); - - if(def) { - shower.attr('data-curopt', btn_opts[def].sel); - } else if(!shower.attr('data-curopt')) { - // Set first as default - shower.attr('data-curopt', btn_opts[0].sel); - } - - var timer; - - var pos = $(show_sel).position(); - $(hold_sel).css({'left': pos.left+34, 'top': pos.top+40}); - - // Clicking the "show" icon should set the current mode - shower.mousedown(function(evt) { - if(shower.hasClass('disabled')) return false; - var holder = $(hold_sel); - var l = pos.left+34; - var w = holder.width()*-1; - var time = holder.data('shown_popop')?200:0; - timer = setTimeout(function() { - // Show corresponding menu - if(!shower.data('isLibrary')) { - holder.css('left', w).show().animate({ - left: l - },150); - } else { - holder.css('left', l).show(); - } - holder.data('shown_popop',true); - },time); - evt.preventDefault(); - }).mouseup(function(evt) { - clearTimeout(timer); - var opt = $(this).attr('data-curopt'); - // Is library and popped up, so do nothing - if(shower.data('isLibrary') && $(show_sel.replace('_show','')).is(':visible')) { - toolButtonClick(show_sel, true); - return; - } - if (toolButtonClick(show_sel) && (opt in flyout_funcs)) { - flyout_funcs[opt](); - } - }); - - // $('#tools_rect').mouseleave(function(){$('#tools_rect').fadeOut();}); - }); - - setFlyoutTitles(); - } - - var makeFlyoutHolder = function(id, child) { - var div = $('
    ',{ - 'class': 'tools_flyout', - id: id - }).appendTo('#svg_editor').append(child); - - return div; - } - - var setFlyoutPositions = function() { - $('.tools_flyout').each(function() { - var shower = $('#' + this.id + '_show'); - var pos = shower.offset(); - var w = shower.outerWidth(); - $(this).css({left: (pos.left + w)*tool_scale, top: pos.top}); - }); - } - - var setFlyoutTitles = function() { - $('.tools_flyout').each(function() { - var shower = $('#' + this.id + '_show'); - if(shower.data('isLibrary')) return; - - var tooltips = []; - $(this).children().each(function() { - tooltips.push(this.title); - }); - shower[0].title = tooltips.join(' / '); - }); - } - - var resize_timer; - - var extAdded = function(window, ext) { - - var cb_called = false; - var resize_done = false; - var cb_ready = true; // Set to false to delay callback (e.g. wait for $.svgIcons) - - function prepResize() { - if(resize_timer) { - clearTimeout(resize_timer); - resize_timer = null; - } - if(!resize_done) { - resize_timer = setTimeout(function() { - resize_done = true; - setIconSize(curPrefs.iconsize); - }, 50); - } - } - - - var runCallback = function() { - if(ext.callback && !cb_called && cb_ready) { - cb_called = true; - ext.callback(); - } - } - - var btn_selects = []; - - if(ext.context_tools) { - $.each(ext.context_tools, function(i, tool) { - // Add select tool - var cont_id = tool.container_id?(' id="' + tool.container_id + '"'):""; - - var panel = $('#' + tool.panel); - - // create the panel if it doesn't exist - if(!panel.length) - panel = $('
    ', {id: tool.panel}).appendTo("#tools_top"); - - // TODO: Allow support for other types, or adding to existing tool - switch (tool.type) { - case 'tool_button': - var html = '
    ' + tool.id + '
    '; - var div = $(html).appendTo(panel); - if (tool.events) { - $.each(tool.events, function(evt, func) { - $(div).bind(evt, func); - }); - } - break; - case 'select': - var html = '' - + '"; - // Creates the tool, hides & adds it, returns the select element - var sel = $(html).appendTo(panel).find('select'); - - $.each(tool.events, function(evt, func) { - $(sel).bind(evt, func); - }); - break; - case 'button-select': - var html = ''; - - var list = $('
      ').appendTo('#option_lists'); - - if(tool.colnum) { - list.addClass('optcols' + tool.colnum); - } - - // Creates the tool, hides & adds it, returns the select element - var dropdown = $(html).appendTo(panel).children(); - - btn_selects.push({ - elem: ('#' + tool.id), - list: ('#' + tool.id + '_opts'), - title: tool.title, - callback: tool.events.change, - cur: ('#cur_' + tool.id) - }); - - break; - case 'input': - var html = '' - + '' - + tool.label + ':' - + '' - - // Creates the tool, hides & adds it, returns the select element - - // Add to given tool.panel - var inp = $(html).appendTo(panel).find('input'); - - if(tool.spindata) { - inp.SpinButton(tool.spindata); - } - - if(tool.events) { - $.each(tool.events, function(evt, func) { - inp.bind(evt, func); - }); - } - break; - - default: - break; - } - }); - } - - if(ext.buttons) { - var fallback_obj = {}, - placement_obj = {}, - svgicons = ext.svgicons; - var holders = {}; - - - // Add buttons given by extension - $.each(ext.buttons, function(i, btn) { - var icon; - var id = btn.id; - var num = i; - - // Give button a unique ID - while($('#'+id).length) { - id = btn.id + '_' + (++num); - } - - if(!svgicons) { - icon = $(''); - } else { - fallback_obj[id] = btn.icon; - var svgicon = btn.svgicon?btn.svgicon:btn.id; - if(btn.type == 'app_menu') { - placement_obj['#' + id + ' > div'] = svgicon; - } else { - placement_obj['#' + id] = svgicon; - } - } - - var cls, parent; - - // Set button up according to its type - switch ( btn.type ) { - case 'mode_flyout': - case 'mode': - cls = 'tool_button'; - parent = "#tools_left"; - break; - case 'context': - cls = 'tool_button'; - parent = "#" + btn.panel; - // create the panel if it doesn't exist - if(!$(parent).length) - $('
      ', {id: btn.panel}).appendTo("#tools_top"); - break; - case 'app_menu': - cls = ''; - parent = '#main_menu ul'; - break; - } - - var button = $((btn.list || btn.type == 'app_menu')?'
    • ':'
      ') - .attr("id", id) - .attr("title", btn.title) - .addClass(cls); - if(!btn.includeWith && !btn.list) { - if("position" in btn) { - $(parent).children().eq(btn.position).before(button); - } else { - button.appendTo(parent); - } - - if(btn.type =='mode_flyout') { - // Add to flyout menu / make flyout menu - // var opts = btn.includeWith; - // // opts.button, default, position - var ref_btn = $(button); - - var flyout_holder = ref_btn.parent(); - // Create a flyout menu if there isn't one already - if(!ref_btn.parent().hasClass('tools_flyout')) { - // Create flyout placeholder - var tls_id = ref_btn[0].id.replace('tool_','tools_') - var show_btn = ref_btn.clone() - .attr('id',tls_id + '_show') - .append($('
      ',{'class':'flyout_arrow_horiz'})); - - ref_btn.before(show_btn); - - // Create a flyout div - flyout_holder = makeFlyoutHolder(tls_id, ref_btn); - flyout_holder.data('isLibrary', true); - show_btn.data('isLibrary', true); - } - - - - // var ref_data = Actions.getButtonData(opts.button); - - placement_obj['#' + tls_id + '_show'] = btn.id; - // TODO: Find way to set the current icon using the iconloader if this is not default - - // Include data for extension button as well as ref button - var cur_h = holders['#'+flyout_holder[0].id] = [{ - sel: '#'+id, - fn: btn.events.click, - icon: btn.id, -// key: btn.key, - isDefault: true - }, ref_data]; - // - // // {sel:'#tool_rect', fn: clickRect, evt: 'mouseup', key: 4, parent: '#tools_rect', icon: 'rect'} - // - // var pos = ("position" in opts)?opts.position:'last'; - // var len = flyout_holder.children().length; - // - // // Add at given position or end - // if(!isNaN(pos) && pos >= 0 && pos < len) { - // flyout_holder.children().eq(pos).before(button); - // } else { - // flyout_holder.append(button); - // cur_h.reverse(); - // } - } else if(btn.type == 'app_menu') { - button.append('
      ').append(btn.title); - } - - } else if(btn.list) { - // Add button to list - button.addClass('push_button'); - $('#' + btn.list + '_opts').append(button); - if(btn.isDefault) { - $('#cur_' + btn.list).append(button.children().clone()); - var svgicon = btn.svgicon?btn.svgicon:btn.id; - placement_obj['#cur_' + btn.list] = svgicon; - } - } else if(btn.includeWith) { - // Add to flyout menu / make flyout menu - var opts = btn.includeWith; - // opts.button, default, position - var ref_btn = $(opts.button); - - var flyout_holder = ref_btn.parent(); - // Create a flyout menu if there isn't one already - if(!ref_btn.parent().hasClass('tools_flyout')) { - // Create flyout placeholder - var tls_id = ref_btn[0].id.replace('tool_','tools_') - var show_btn = ref_btn.clone() - .attr('id',tls_id + '_show') - .append($('
      ',{'class':'flyout_arrow_horiz'})); - - ref_btn.before(show_btn); - - // Create a flyout div - flyout_holder = makeFlyoutHolder(tls_id, ref_btn); - } - - var ref_data = Actions.getButtonData(opts.button); - - if(opts.isDefault) { - placement_obj['#' + tls_id + '_show'] = btn.id; - } - // TODO: Find way to set the current icon using the iconloader if this is not default - - // Include data for extension button as well as ref button - var cur_h = holders['#'+flyout_holder[0].id] = [{ - sel: '#'+id, - fn: btn.events.click, - icon: btn.id, - key: btn.key, - isDefault: btn.includeWith?btn.includeWith.isDefault:0 - }, ref_data]; - - // {sel:'#tool_rect', fn: clickRect, evt: 'mouseup', key: 4, parent: '#tools_rect', icon: 'rect'} - - var pos = ("position" in opts)?opts.position:'last'; - var len = flyout_holder.children().length; - - // Add at given position or end - if(!isNaN(pos) && pos >= 0 && pos < len) { - flyout_holder.children().eq(pos).before(button); - } else { - flyout_holder.append(button); - cur_h.reverse(); - } - } - - if(!svgicons) { - button.append(icon); - } - - if(!btn.list) { - // Add given events to button - $.each(btn.events, function(name, func) { - if(name == "click") { - if(btn.type == 'mode') { - if(btn.includeWith) { - button.bind(name, func); - } else { - button.bind(name, function() { - if(toolButtonClick(button)) { - func(); - } - }); - } - if(btn.key) { - $(document).bind('keydown', btn.key, func); - if(btn.title) button.attr("title", btn.title + ' ['+btn.key+']'); - } - } else { - button.bind(name, func); - } - } else { - button.bind(name, func); - } - }); - } - - setupFlyouts(holders); - }); - - $.each(btn_selects, function() { - addAltDropDown(this.elem, this.list, this.callback, {seticon: true}); - }); - - if (svgicons) - cb_ready = false; // Delay callback - - $.svgIcons(svgicons, { - w:24, h:24, - id_match: false, - no_img: (!isWebkit), - fallback: fallback_obj, - placement: placement_obj, - callback: function(icons) { - // Non-ideal hack to make the icon match the current size - if(curPrefs.iconsize && curPrefs.iconsize != 'm') { - prepResize(); - } - cb_ready = true; // Ready for callback - runCallback(); - } - - }); - } - - runCallback(); - }; - - var getPaint = function(color, opac, type) { - // update the editor's fill paint - var opts = null; - if (color.indexOf("url(#") === 0) { - var refElem = svgCanvas.getRefElem(color); - if(refElem) { - refElem = refElem.cloneNode(true); - } else { - refElem = $("#" + type + "_color defs *")[0]; - } - - opts = { alpha: opac }; - opts[refElem.tagName] = refElem; - } - else if (color.indexOf("#") === 0) { - opts = { - alpha: opac, - solidColor: color.substr(1) - }; - } - else { - opts = { - alpha: opac, - solidColor: 'none' - }; - } - return new $.jGraduate.Paint(opts); - }; - - - // updates the toolbar (colors, opacity, etc) based on the selected element - // This function also updates the opacity and id elements that are in the context panel - var updateToolbar = function() { - if (selectedElement != null) { - - switch ( selectedElement.tagName ) { - case 'use': - case 'image': - case 'foreignObject': - break; - case 'g': - case 'a': - // Look for common styles - - var gWidth = null; - - var childs = selectedElement.getElementsByTagName('*'); - for(var i = 0, len = childs.length; i < len; i++) { - var swidth = childs[i].getAttribute("stroke-width"); - - if(i === 0) { - gWidth = swidth; - } else if(gWidth !== swidth) { - gWidth = null; - } - } - - $('#stroke_width').val(gWidth === null ? "" : gWidth); - - paintBox.fill.update(true); - paintBox.stroke.update(true); - - - break; - default: - paintBox.fill.update(true); - paintBox.stroke.update(true); - //console.log(paintBox.fill); - - $('#stroke_width').val(selectedElement.getAttribute("stroke-width") || 1); - $('#stroke_style').val(selectedElement.getAttribute("stroke-dasharray")||"none"); - - var attr = selectedElement.getAttribute("stroke-linejoin") || 'miter'; - - if ($('#linejoin_' + attr).length != 0) - setStrokeOpt($('#linejoin_' + attr)[0]); - - attr = selectedElement.getAttribute("stroke-linecap") || 'butt'; - - if ($('#linecap_' + attr).length != 0) - setStrokeOpt($('#linecap_' + attr)[0]); - } - - } - - // All elements including image and group have opacity - if(selectedElement != null) { - var opac_perc = ((selectedElement.getAttribute("opacity")||1.0)*100); - $('#group_opacity').val(opac_perc); - $('#opac_slider').slider('option', 'value', opac_perc); - $('#elem_id').val(selectedElement.id); - } - - updateToolButtonState(); - }; - - var setImageURL = Editor.setImageURL = function(url) { - if(!url) url = default_img_url; - - svgCanvas.setImageURL(url); - $('#image_url').val(url); - - if(url.indexOf('data:') === 0) { - // data URI found - $('#image_url').hide(); - $('#change_image_url').show(); - } else { - // regular URL - - svgCanvas.embedImage(url, function(datauri) { - if(!datauri) { - // Couldn't embed, so show warning - $('#url_notice').show(); - } else { - $('#url_notice').hide(); - } - default_img_url = url; - }); - $('#image_url').show(); - $('#change_image_url').hide(); - } - } - - var setInputWidth = function(elem) { - var w = Math.min(Math.max(12 + elem.value.length * 6, 50), 300); - $(elem).width(w); - } - - // updates the context panel tools based on the selected element - var updateContextPanel = function() { - var elem = selectedElement; - // If element has just been deleted, consider it null - if(elem != null && !elem.parentNode) elem = null; - var currentLayerName = svgCanvas.getCurrentDrawing().getCurrentLayerName(); - var currentMode = svgCanvas.getMode(); - var unit = curConfig.baseUnit !== 'px' ? curConfig.baseUnit : null; - - var is_node = currentMode == 'pathedit'; //elem ? (elem.id && elem.id.indexOf('pathpointgrip') == 0) : false; - var menu_items = $('#cmenu_canvas li'); - $('#selected_panel, #multiselected_panel, #g_panel, #rect_panel, #circle_panel,\ - #ellipse_panel, #line_panel, #text_panel, #image_panel, #container_panel, #use_panel, #a_panel').hide(); - if (elem != null) { - var elname = elem.nodeName; - - // If this is a link with no transform and one child, pretend - // its child is selected -// console.log('go', elem) -// if(elname === 'a') { // && !$(elem).attr('transform')) { -// elem = elem.firstChild; -// } - - var angle = svgCanvas.getRotationAngle(elem); - $('#angle').val(angle); - - var blurval = svgCanvas.getBlur(elem); - $('#blur').val(blurval); - $('#blur_slider').slider('option', 'value', blurval); - - if(svgCanvas.addedNew) { - if(elname === 'image') { - // Prompt for URL if not a data URL - if(svgCanvas.getHref(elem).indexOf('data:') !== 0) { - promptImgURL(); - } - } /*else if(elname == 'text') { - // TODO: Do something here for new text - }*/ - } - - if(!is_node && currentMode != 'pathedit') { - $('#selected_panel').show(); - // Elements in this array already have coord fields - if(['line', 'circle', 'ellipse'].indexOf(elname) >= 0) { - $('#xy_panel').hide(); - } else { - var x,y; - - // Get BBox vals for g, polyline and path - if(['g', 'polyline', 'path'].indexOf(elname) >= 0) { - var bb = svgCanvas.getStrokedBBox([elem]); - if(bb) { - x = bb.x; - y = bb.y; - } - } else { - x = elem.getAttribute('x'); - y = elem.getAttribute('y'); - } - - if(unit) { - x = svgedit.units.convertUnit(x); - y = svgedit.units.convertUnit(y); - } - - $('#selected_x').val(x || 0); - $('#selected_y').val(y || 0); - $('#xy_panel').show(); - } - - // Elements in this array cannot be converted to a path - var no_path = ['image', 'text', 'path', 'g', 'use'].indexOf(elname) == -1; - $('#tool_topath').toggle(no_path); - $('#tool_reorient').toggle(elname == 'path'); - $('#tool_reorient').toggleClass('disabled', angle == 0); - } else { - var point = path.getNodePoint(); - $('#tool_add_subpath').removeClass('push_button_pressed').addClass('tool_button'); - $('#tool_node_delete').toggleClass('disabled', !path.canDeleteNodes); - - // Show open/close button based on selected point - setIcon('#tool_openclose_path', path.closed_subpath ? 'open_path' : 'close_path'); - - if(point) { - var seg_type = $('#seg_type'); - if(unit) { - point.x = svgedit.units.convertUnit(point.x); - point.y = svgedit.units.convertUnit(point.y); - } - $('#path_node_x').val(point.x); - $('#path_node_y').val(point.y); - if(point.type) { - seg_type.val(point.type).removeAttr('disabled'); - } else { - seg_type.val(4).attr('disabled','disabled'); - } - } - return; - } - - // update contextual tools here - var panels = { - g: [], - a: [], - rect: ['rx','width','height'], - image: ['width','height'], - circle: ['cx','cy','r'], - ellipse: ['cx','cy','rx','ry'], - line: ['x1','y1','x2','y2'], - text: [], - 'use': [] - }; - - var el_name = elem.tagName; - -// if($(elem).data('gsvg')) { -// $('#g_panel').show(); -// } - - var link_href = null; - if (el_name === 'a') { - link_href = svgCanvas.getHref(elem); - $('#g_panel').show(); - } - - if(elem.parentNode.tagName === 'a') { - if(!$(elem).siblings().length) { - $('#a_panel').show(); - link_href = svgCanvas.getHref(elem.parentNode); - } - } - - // Hide/show the make_link buttons - $('#tool_make_link, #tool_make_link').toggle(!link_href); - - if(link_href) { - $('#link_url').val(link_href); - } - - if(panels[el_name]) { - var cur_panel = panels[el_name]; - - $('#' + el_name + '_panel').show(); - - $.each(cur_panel, function(i, item) { - var attrVal = elem.getAttribute(item); - if(curConfig.baseUnit !== 'px' && elem[item]) { - var bv = elem[item].baseVal.value; - attrVal = svgedit.units.convertUnit(bv); - } - - $('#' + el_name + '_' + item).val(attrVal || 0); - }); - - if(el_name == 'text') { - $('#text_panel').css("display", "inline"); - if (svgCanvas.getItalic()) { - $('#tool_italic').addClass('push_button_pressed').removeClass('tool_button'); - } - else { - $('#tool_italic').removeClass('push_button_pressed').addClass('tool_button'); - } - if (svgCanvas.getBold()) { - $('#tool_bold').addClass('push_button_pressed').removeClass('tool_button'); - } - else { - $('#tool_bold').removeClass('push_button_pressed').addClass('tool_button'); - } - $('#font_family').val(elem.getAttribute("font-family")); - $('#font_size').val(elem.getAttribute("font-size")); - $('#text').val(elem.textContent); - if (svgCanvas.addedNew) { - // Timeout needed for IE9 - setTimeout(function() { - $('#text').focus().select(); - },100); - } - } // text - else if(el_name == 'image') { - setImageURL(svgCanvas.getHref(elem)); - } // image - else if(el_name === 'g' || el_name === 'use') { - $('#container_panel').show(); - var title = svgCanvas.getTitle(); - var label = $('#g_title')[0]; - label.value = title; - setInputWidth(label); - var d = 'disabled'; - if(el_name == 'use') { - label.setAttribute(d, d); - } else { - label.removeAttribute(d); - } - } - } - menu_items[(el_name === 'g' ? 'en':'dis') + 'ableContextMenuItems']('#ungroup'); - menu_items[((el_name === 'g' || !multiselected) ? 'dis':'en') + 'ableContextMenuItems']('#group'); - } // if (elem != null) - else if (multiselected) { - $('#multiselected_panel').show(); - menu_items - .enableContextMenuItems('#group') - .disableContextMenuItems('#ungroup'); - } else { - menu_items.disableContextMenuItems('#delete,#cut,#copy,#group,#ungroup,#move_front,#move_up,#move_down,#move_back'); - } - - // update history buttons - if (undoMgr.getUndoStackSize() > 0) { - $('#tool_undo').removeClass( 'disabled'); - } - else { - $('#tool_undo').addClass( 'disabled'); - } - if (undoMgr.getRedoStackSize() > 0) { - $('#tool_redo').removeClass( 'disabled'); - } - else { - $('#tool_redo').addClass( 'disabled'); - } - - svgCanvas.addedNew = false; - - if ( (elem && !is_node) || multiselected) { - // update the selected elements' layer - $('#selLayerNames').removeAttr('disabled').val(currentLayerName); - - // Enable regular menu options - canv_menu.enableContextMenuItems('#delete,#cut,#copy,#move_front,#move_up,#move_down,#move_back'); - } - else { - $('#selLayerNames').attr('disabled', 'disabled'); - } - }; - - $('#text').focus( function(){ textBeingEntered = true; } ); - $('#text').blur( function(){ textBeingEntered = false; } ); - - // bind the selected event to our function that handles updates to the UI - svgCanvas.bind("selected", selectedChanged); - svgCanvas.bind("transition", elementTransition); - svgCanvas.bind("changed", elementChanged); - svgCanvas.bind("saved", saveHandler); - svgCanvas.bind("exported", exportHandler); - svgCanvas.bind("zoomed", zoomChanged); - svgCanvas.bind("contextset", contextChanged); - svgCanvas.bind("extension_added", extAdded); - svgCanvas.textActions.setInputElem($("#text")[0]); - - var str = '
      ' - $.each(palette, function(i,item){ - str += '
      '; - }); - $('#palette').append(str); - - // Set up editor background functionality - // TODO add checkerboard as "pattern" - var color_blocks = ['#FFF','#888','#000']; // ,'url(data:image/gif;base64,R0lGODlhEAAQAIAAAP%2F%2F%2F9bW1iH5BAAAAAAALAAAAAAQABAAAAIfjG%2Bgq4jM3IFLJgpswNly%2FXkcBpIiVaInlLJr9FZWAQA7)']; - var str = ''; - $.each(color_blocks, function() { - str += '
      '; - }); - $('#bg_blocks').append(str); - var blocks = $('#bg_blocks div'); - var cur_bg = 'cur_background'; - blocks.each(function() { - var blk = $(this); - blk.click(function() { - blocks.removeClass(cur_bg); - $(this).addClass(cur_bg); - }); - }); - - if($.pref('bkgd_color')) { - setBackground($.pref('bkgd_color'), $.pref('bkgd_url')); - } else if($.pref('bkgd_url')) { - // No color set, only URL - setBackground(defaultPrefs.bkgd_color, $.pref('bkgd_url')); - } - - if($.pref('img_save')) { - curPrefs.img_save = $.pref('img_save'); - $('#image_save_opts input').val([curPrefs.img_save]); - } - - var changeRectRadius = function(ctl) { - svgCanvas.setRectRadius(ctl.value); - } - - var changeFontSize = function(ctl) { - svgCanvas.setFontSize(ctl.value); - } - - var changeStrokeWidth = function(ctl) { - var val = ctl.value; - if(val == 0 && selectedElement && ['line', 'polyline'].indexOf(selectedElement.nodeName) >= 0) { - val = ctl.value = 1; - } - svgCanvas.setStrokeWidth(val); - } - - var changeRotationAngle = function(ctl) { - svgCanvas.setRotationAngle(ctl.value); - $('#tool_reorient').toggleClass('disabled', ctl.value == 0); - } - var changeZoom = function(ctl) { - var zoomlevel = ctl.value / 100; - if(zoomlevel < .001) { - ctl.value = .1; - return; - } - var zoom = svgCanvas.getZoom(); - var w_area = workarea; - - zoomChanged(window, { - width: 0, - height: 0, - // center pt of scroll position - x: (w_area[0].scrollLeft + w_area.width()/2)/zoom, - y: (w_area[0].scrollTop + w_area.height()/2)/zoom, - zoom: zoomlevel - }, true); - } - - var changeOpacity = function(ctl, val) { - if(val == null) val = ctl.value; - $('#group_opacity').val(val); - if(!ctl || !ctl.handle) { - $('#opac_slider').slider('option', 'value', val); - } - svgCanvas.setOpacity(val/100); - } - - var changeBlur = function(ctl, val, noUndo) { - if(val == null) val = ctl.value; - $('#blur').val(val); - var complete = false; - if(!ctl || !ctl.handle) { - $('#blur_slider').slider('option', 'value', val); - complete = true; - } - if(noUndo) { - svgCanvas.setBlurNoUndo(val); - } else { - svgCanvas.setBlur(val, complete); - } - } - - var operaRepaint = function() { - // Repaints canvas in Opera. Needed for stroke-dasharray change as well as fill change - if(!window.opera) return; - $('

      ').hide().appendTo('body').remove(); - } - - $('#stroke_style').change(function(){ - svgCanvas.setStrokeAttr('stroke-dasharray', $(this).val()); - operaRepaint(); - }); - - $('#stroke_linejoin').change(function(){ - svgCanvas.setStrokeAttr('stroke-linejoin', $(this).val()); - operaRepaint(); - }); - - - // Lose focus for select elements when changed (Allows keyboard shortcuts to work better) - $('select').change(function(){$(this).blur();}); - - // fired when user wants to move elements to another layer - var promptMoveLayerOnce = false; - $('#selLayerNames').change(function(){ - var destLayer = this.options[this.selectedIndex].value; - var confirm_str = uiStrings.notification.QmoveElemsToLayer.replace('%s',destLayer); - var moveToLayer = function(ok) { - if(!ok) return; - promptMoveLayerOnce = true; - svgCanvas.moveSelectedToLayer(destLayer); - svgCanvas.clearSelection(); - populateLayers(); - } - if (destLayer) { - if(promptMoveLayerOnce) { - moveToLayer(true); - } else { - $.confirm(confirm_str, moveToLayer); - } - } - }); - - $('#font_family').change(function() { - svgCanvas.setFontFamily(this.value); - }); - - $('#seg_type').change(function() { - svgCanvas.setSegType($(this).val()); - }); - - $('#text').keyup(function(){ - svgCanvas.setTextContent(this.value); - }); - - $('#image_url').change(function(){ - setImageURL(this.value); - }); - - $('#link_url').change(function() { - if(this.value.length) { - svgCanvas.setLinkURL(this.value); - } else { - svgCanvas.removeHyperlink(); - } - }); - - $('#g_title').change(function() { - svgCanvas.setGroupTitle(this.value); - }); - - $('.attr_changer').change(function() { - var attr = this.getAttribute("data-attr"); - var val = this.value; - var valid = svgedit.units.isValidUnit(attr, val, selectedElement); - - if(!valid) { - $.alert(uiStrings.notification.invalidAttrValGiven); - this.value = selectedElement.getAttribute(attr); - return false; - } - - if (attr !== "id") { - if (isNaN(val)) { - val = svgCanvas.convertToNum(attr, val); - } else if(curConfig.baseUnit !== 'px') { - // Convert unitless value to one with given unit - - var unitData = svgedit.units.getTypeMap(); - - if(selectedElement[attr] || svgCanvas.getMode() === "pathedit" || attr === "x" || attr === "y") { - val *= unitData[curConfig.baseUnit]; - } - } - } - - // if the user is changing the id, then de-select the element first - // change the ID, then re-select it with the new ID - if (attr === "id") { - var elem = selectedElement; - svgCanvas.clearSelection(); - elem.id = val; - svgCanvas.addToSelection([elem],true); - } - else { - svgCanvas.changeSelectedAttribute(attr, val); - } - this.blur(); - }); - - // Prevent selection of elements when shift-clicking - $('#palette').mouseover(function() { - var inp = $(''); - $(this).append(inp); - inp.focus().remove(); - }) - - $('.palette_item').mousedown(function(evt){ - var right_click = evt.button === 2; - var isStroke = evt.shiftKey || right_click; - var picker = isStroke ? "stroke" : "fill"; - var color = $(this).attr('data-rgb'); - var paint = null; - - // Webkit-based browsers returned 'initial' here for no stroke - if (color === 'none' || color === 'transparent' || color === 'initial') { - color = 'none'; - paint = new $.jGraduate.Paint(); - } - else { - paint = new $.jGraduate.Paint({alpha: 100, solidColor: color.substr(1)}); - } - - paintBox[picker].setPaint(paint); - - if (isStroke) { - svgCanvas.setColor('stroke', color); - if (color != 'none' && svgCanvas.getStrokeOpacity() != 1) { - svgCanvas.setPaintOpacity('stroke', 1.0); - } - } else { - svgCanvas.setColor('fill', color); - if (color != 'none' && svgCanvas.getFillOpacity() != 1) { - svgCanvas.setPaintOpacity('fill', 1.0); - } - } - updateToolButtonState(); - }).bind('contextmenu', function(e) {e.preventDefault()}); - - $("#toggle_stroke_tools").on("click", function() { - $("#tools_bottom").toggleClass("expanded"); - }); - - // This is a common function used when a tool has been clicked (chosen) - // It does several common things: - // - removes the tool_button_current class from whatever tool currently has it - // - hides any flyouts - // - adds the tool_button_current class to the button passed in - var toolButtonClick = Editor.toolButtonClick = function(button, noHiding) { - if ($(button).hasClass('disabled')) return false; - if($(button).parent().hasClass('tools_flyout')) return true; - var fadeFlyouts = fadeFlyouts || 'normal'; - if(!noHiding) { - $('.tools_flyout').fadeOut(fadeFlyouts); - } - $('#styleoverrides').text(''); - workarea.css('cursor','auto'); - $('.tool_button_current').removeClass('tool_button_current').addClass('tool_button'); - $(button).addClass('tool_button_current').removeClass('tool_button'); - return true; - }; - - (function() { - var last_x = null, last_y = null, w_area = workarea[0], - panning = false, keypan = false; - - $('#svgcanvas').bind('mousemove mouseup', function(evt) { - if(panning === false) return; - - w_area.scrollLeft -= (evt.clientX - last_x); - w_area.scrollTop -= (evt.clientY - last_y); - - last_x = evt.clientX; - last_y = evt.clientY; - - if(evt.type === 'mouseup') panning = false; - return false; - }).mousedown(function(evt) { - if(evt.button === 1 || keypan === true) { - panning = true; - last_x = evt.clientX; - last_y = evt.clientY; - return false; - } - }); - - $(window).mouseup(function() { - panning = false; - }); - - $(document).bind('keydown', 'space', function(evt) { - svgCanvas.spaceKey = keypan = true; - evt.preventDefault(); - }).bind('keyup', 'space', function(evt) { - evt.preventDefault(); - svgCanvas.spaceKey = keypan = false; - }).bind('keydown', 'shift', function(evt) { - if(svgCanvas.getMode() === 'zoom') { - workarea.css('cursor', zoomOutIcon); - } - }).bind('keyup', 'shift', function(evt) { - if(svgCanvas.getMode() === 'zoom') { - workarea.css('cursor', zoomInIcon); - } - }) - - Editor.setPanning = function(active) { - svgCanvas.spaceKey = keypan = active; - }; - }()); - - - function setStrokeOpt(opt, changeElem) { - var id = opt.id; - var bits = id.split('_'); - var pre = bits[0]; - var val = bits[1]; - - if(changeElem) { - svgCanvas.setStrokeAttr('stroke-' + pre, val); - } - operaRepaint(); - setIcon('#cur_' + pre , id, 20); - $(opt).addClass('current').siblings().removeClass('current'); - } - - (function() { - var button = $('#main_icon'); - var overlay = $('#main_icon span'); - var list = $('#main_menu'); - var on_button = false; - var height = 0; - var js_hover = true; - var set_click = false; - - var hideMenu = function() { - list.fadeOut(200); - }; - - $(window).mouseup(function(evt) { - if(!on_button) { - button.removeClass('buttondown'); - // do not hide if it was the file input as that input needs to be visible - // for its change event to fire - if (evt.target.tagName != "INPUT") { - list.fadeOut(200); - } else if(!set_click) { - set_click = true; - $(evt.target).click(function() { - list.css('margin-left','-9999px').show(); - }); - } - } - on_button = false; - }).mousedown(function(evt) { -// $(".contextMenu").hide(); -// console.log('cm', $(evt.target).closest('.contextMenu')); - - var islib = $(evt.target).closest('div.tools_flyout, .contextMenu').length; - if(!islib) $('.tools_flyout:visible,.contextMenu').fadeOut(250); - }); - - overlay.bind('mousedown',function() { - if (!button.hasClass('buttondown')) { - button.addClass('buttondown').removeClass('buttonup') - // Margin must be reset in case it was changed before; - list.css('margin-left',0).show(); - if(!height) { - height = list.height(); - } - // Using custom animation as slideDown has annoying "bounce effect" - list.css('height',0).animate({ - 'height': height - },200); - on_button = true; - return false; - } else { - button.removeClass('buttondown').addClass('buttonup'); - list.fadeOut(200); - } - }).hover(function() { - on_button = true; - }).mouseout(function() { - on_button = false; - }); - - var list_items = $('#main_menu li'); - - // Check if JS method of hovering needs to be used (Webkit bug) - list_items.mouseover(function() { - js_hover = ($(this).css('background-color') == 'rgba(0, 0, 0, 0)'); - - list_items.unbind('mouseover'); - if(js_hover) { - list_items.mouseover(function() { - this.style.backgroundColor = '#FFC'; - }).mouseout(function() { - this.style.backgroundColor = 'transparent'; - return true; - }); - } - }); - }()); - // Made public for UI customization. - // TODO: Group UI functions into a public svgEditor.ui interface. - Editor.addDropDown = function(elem, callback, dropUp) { - if ($(elem).length == 0) return; // Quit if called on non-existant element - var button = $(elem).find('button'); - - var list = $(elem).find('ul').attr('id', $(elem)[0].id + '-list'); - - if(!dropUp) { - // Move list to place where it can overflow container - $('#option_lists').append(list); - } - - var on_button = false; - if(dropUp) { - $(elem).addClass('dropup'); - } - - list.find('li').bind('mouseup', callback); - - $(window).mouseup(function(evt) { - if(!on_button) { - button.removeClass('down'); - list.hide(); - } - on_button = false; - }); - - button.bind('mousedown',function() { - if (!button.hasClass('down')) { - button.addClass('down'); - - if(!dropUp) { - var pos = $(elem).position(); - list.css({ - top: pos.top + 24, - left: pos.left - 10 - }); - } - list.show(); - - on_button = true; - } else { - button.removeClass('down'); - list.hide(); - } - }).hover(function() { - on_button = true; - }).mouseout(function() { - on_button = false; - }); - } - - // TODO: Combine this with addDropDown or find other way to optimize - var addAltDropDown = function(elem, list, callback, opts) { - var button = $(elem); - var list = $(list); - var on_button = false; - var dropUp = opts.dropUp; - if(dropUp) { - $(elem).addClass('dropup'); - } - list.find('li').bind('mouseup', function() { - if(opts.seticon) { - setIcon('#cur_' + button[0].id , $(this).children()); - $(this).addClass('current').siblings().removeClass('current'); - } - callback.apply(this, arguments); - - }); - - $(window).mouseup(function(evt) { - if(!on_button) { - button.removeClass('down'); - list.hide(); - list.css({top:0, left:0}); - } - on_button = false; - }); - - var height = list.height(); - $(elem).bind('mousedown',function() { - var off = $(elem).offset(); - if(dropUp) { - off.top -= list.height(); - off.left += 8; - } else { - off.top += $(elem).height(); - } - $(list).offset(off); - - if (!button.hasClass('down')) { - button.addClass('down'); - list.show(); - on_button = true; - return false; - } else { - button.removeClass('down'); - // CSS position must be reset for Webkit - list.hide(); - list.css({top:0, left:0}); - } - }).hover(function() { - on_button = true; - }).mouseout(function() { - on_button = false; - }); - - if(opts.multiclick) { - list.mousedown(function() { - on_button = true; - }); - } - } - - Editor.addDropDown('#font_family_dropdown', function() { - var fam = $(this).text(); - $('#font_family').val($(this).text()).change(); - }); - - Editor.addDropDown('#opacity_dropdown', function() { - if($(this).find('div').length) return; - var perc = parseInt($(this).text().split('%')[0]); - changeOpacity(false, perc); - }, true); - - // For slider usage, see: http://jqueryui.com/demos/slider/ - $("#opac_slider").slider({ - start: function() { - $('#opacity_dropdown li:not(.special)').hide(); - }, - stop: function() { - $('#opacity_dropdown li').show(); - $(window).mouseup(); - }, - slide: function(evt, ui){ - changeOpacity(ui); - } - }); - - Editor.addDropDown('#blur_dropdown', $.noop); - - var slideStart = false; - - $("#blur_slider").slider({ - max: 10, - step: .1, - stop: function(evt, ui) { - slideStart = false; - changeBlur(ui); - $('#blur_dropdown li').show(); - $(window).mouseup(); - }, - start: function() { - slideStart = true; - }, - slide: function(evt, ui){ - changeBlur(ui, null, slideStart); - } - }); - - - Editor.addDropDown('#zoom_dropdown', function() { - var item = $(this); - var val = item.attr('data-val'); - if(val) { - zoomChanged(window, val); - } else { - changeZoom({value:parseInt(item.text())}); - } - }, true); - - addAltDropDown('#stroke_linecap', '#linecap_opts', function() { - setStrokeOpt(this, true); - }, {dropUp: true}); - - addAltDropDown('#stroke_linejoin', '#linejoin_opts', function() { - setStrokeOpt(this, true); - }, {dropUp: true}); - - addAltDropDown('#tool_position', '#position_opts', function() { - var letter = this.id.replace('tool_pos','').charAt(0); - svgCanvas.alignSelectedElements(letter, 'page'); - }, {multiclick: true}); - - /* - - When a flyout icon is selected - (if flyout) { - - Change the icon - - Make pressing the button run its stuff - } - - Run its stuff - - When its shortcut key is pressed - - If not current in list, do as above - , else: - - Just run its stuff - - */ - - // Unfocus text input when workarea is mousedowned. - (function() { - var inp; - - var unfocus = function() { - $(inp).blur(); - } - - $('#svg_editor').find('button, select, input:not(#text)').focus(function() { - inp = this; - ui_context = 'toolbars'; - workarea.mousedown(unfocus); - }).blur(function() { - ui_context = 'canvas'; - workarea.unbind('mousedown', unfocus); - // Go back to selecting text if in textedit mode - if(svgCanvas.getMode() == 'textedit') { - $('#text').focus(); - } - }); - - }()); - - var clickSelect = function() { - if (toolButtonClick('#tool_select')) { - svgCanvas.setMode('select'); - $('#styleoverrides').text('#svgcanvas svg *{cursor:move;pointer-events:all}, #svgcanvas svg{cursor:default}'); - } - }; - - var clickFHPath = function() { - if (toolButtonClick('#tool_fhpath')) { - svgCanvas.setMode('fhpath'); - } - }; - - var clickLine = function() { - if (toolButtonClick('#tool_line')) { - svgCanvas.setMode('line'); - } - }; - - var clickSquare = function(){ - if (toolButtonClick('#tool_square')) { - svgCanvas.setMode('square'); - } - }; - - var clickRect = function(){ - if (toolButtonClick('#tool_rect')) { - svgCanvas.setMode('rect'); - } - }; - - var clickFHRect = function(){ - if (toolButtonClick('#tool_fhrect')) { - svgCanvas.setMode('fhrect'); - } - }; - - var clickCircle = function(){ - if (toolButtonClick('#tool_circle')) { - svgCanvas.setMode('circle'); - } - }; - - var clickEllipse = function(){ - if (toolButtonClick('#tool_ellipse')) { - svgCanvas.setMode('ellipse'); - } - }; - - var clickFHEllipse = function(){ - if (toolButtonClick('#tool_fhellipse')) { - svgCanvas.setMode('fhellipse'); - } - }; - - var clickImage = function(){ - if (toolButtonClick('#tool_image')) { - svgCanvas.setMode('image'); - } - }; - - var clickZoom = function(){ - if (toolButtonClick('#tool_zoom')) { - svgCanvas.setMode('zoom'); - workarea.css('cursor', zoomInIcon); - } - }; - - var dblclickZoom = function(){ - if (toolButtonClick('#tool_zoom')) { - zoomImage(); - setSelectMode(); - } - }; - - var clickText = function(){ - if (toolButtonClick('#tool_text')) { - svgCanvas.setMode('text'); - } - }; - - var clickPath = function(){ - if (toolButtonClick('#tool_path')) { - svgCanvas.setMode('path'); - } - }; - - // Delete is a contextual tool that only appears in the ribbon if - // an element has been selected - var deleteSelected = function() { - if (selectedElement != null || multiselected) { - svgCanvas.deleteSelectedElements(); - } - }; - - var cutSelected = function() { - if (selectedElement != null || multiselected) { - svgCanvas.cutSelectedElements(); - } - }; - - var copySelected = function() { - if (selectedElement != null || multiselected) { - svgCanvas.copySelectedElements(); - } - }; - - var pasteInCenter = function() { - var zoom = svgCanvas.getZoom(); - - var x = (workarea[0].scrollLeft + workarea.width()/2)/zoom - svgCanvas.contentW; - var y = (workarea[0].scrollTop + workarea.height()/2)/zoom - svgCanvas.contentH; - svgCanvas.pasteElements('point', x, y); - } - - var moveToTopSelected = function() { - if (selectedElement != null) { - svgCanvas.moveToTopSelectedElement(); - } - }; - - var moveToBottomSelected = function() { - if (selectedElement != null) { - svgCanvas.moveToBottomSelectedElement(); - } - }; - - var moveUpDownSelected = function(dir) { - if (selectedElement != null) { - svgCanvas.moveUpDownSelected(dir); - } - }; - - var convertToPath = function() { - if (selectedElement != null) { - svgCanvas.convertToPath(); - } - } - - var reorientPath = function() { - if (selectedElement != null) { - path.reorient(); - } - } - - var makeHyperlink = function() { - if (selectedElement != null || multiselected) { - $.prompt(uiStrings.notification.enterNewLinkURL, "http://", function(url) { - if(url) svgCanvas.makeHyperlink(url); - }); - } - } - - var moveSelected = function(dx,dy) { - if (selectedElement != null || multiselected) { - if(curConfig.gridSnapping) { - // Use grid snap value regardless of zoom level - var multi = svgCanvas.getZoom() * curConfig.snappingStep; - dx *= multi; - dy *= multi; - } - svgCanvas.moveSelectedElements(dx,dy); - } - }; - - var linkControlPoints = function() { - var linked = !$('#tool_node_link').hasClass('push_button_pressed'); - if (linked) - $('#tool_node_link').addClass('push_button_pressed').removeClass('tool_button'); - else - $('#tool_node_link').removeClass('push_button_pressed').addClass('tool_button'); - - path.linkControlPoints(linked); - } - - var clonePathNode = function() { - if (path.getNodePoint()) { - path.clonePathNode(); - } - }; - - var deletePathNode = function() { - if (path.getNodePoint()) { - path.deletePathNode(); - } - }; - - var addSubPath = function() { - var button = $('#tool_add_subpath'); - var sp = !button.hasClass('push_button_pressed'); - if (sp) { - button.addClass('push_button_pressed').removeClass('tool_button'); - } else { - button.removeClass('push_button_pressed').addClass('tool_button'); - } - - path.addSubPath(sp); - - }; - - var opencloseSubPath = function() { - path.opencloseSubPath(); - } - - var selectNext = function() { - svgCanvas.cycleElement(1); - }; - - var selectPrev = function() { - svgCanvas.cycleElement(0); - }; - - var rotateSelected = function(cw,step) { - if (selectedElement == null || multiselected) return; - if(!cw) step *= -1; - var new_angle = $('#angle').val()*1 + step; - svgCanvas.setRotationAngle(new_angle); - updateContextPanel(); - }; - - var clickClear = function(){ - var dims = curConfig.dimensions; - $.confirm(uiStrings.notification.QwantToClear, function(ok) { - if(!ok) return; - setSelectMode(); - svgCanvas.clear(); - svgCanvas.setResolution(dims[0], dims[1]); - updateCanvas(true); - zoomImage(); - populateLayers(); - updateContextPanel(); - prepPaints(); - svgCanvas.runExtensions('onNewDocument'); - }); - }; - - var clickBold = function(){ - svgCanvas.setBold( !svgCanvas.getBold() ); - updateContextPanel(); - return false; - }; - - var clickItalic = function(){ - svgCanvas.setItalic( !svgCanvas.getItalic() ); - updateContextPanel(); - return false; - }; - - var clickSave = function(){ - // In the future, more options can be provided here - var saveOpts = { - 'images': curPrefs.img_save, - 'round_digits': 6 - } - svgCanvas.save(saveOpts); - }; - - var clickExport = function() { - // Open placeholder window (prevents popup) - if(!customHandlers.pngsave) { - var str = uiStrings.notification.loadingImage; - exportWindow = window.open("data:text/html;charset=utf-8," + str + "<\/title><h1>" + str + "<\/h1>"); - } - - if(window.canvg) { - svgCanvas.rasterExport(); - } else { - $.getScript('canvg/rgbcolor.js', function() { - $.getScript('canvg/canvg.js', function() { - svgCanvas.rasterExport(); - }); - }); - } - } - - // by default, svgCanvas.open() is a no-op. - // it is up to an extension mechanism (opera widget, etc) - // to call setCustomHandlers() which will make it do something - var clickOpen = function(){ - svgCanvas.open(); - }; - var clickImport = function(){ - }; - - var clickUndo = function(){ - if (undoMgr.getUndoStackSize() > 0) { - undoMgr.undo(); - populateLayers(); - } - }; - - var clickRedo = function(){ - if (undoMgr.getRedoStackSize() > 0) { - undoMgr.redo(); - populateLayers(); - } - }; - - var clickGroup = function(){ - // group - if (multiselected) { - svgCanvas.groupSelectedElements(); - } - // ungroup - else if(selectedElement){ - svgCanvas.ungroupSelectedElement(); - } - }; - - var clickClone = function(){ - svgCanvas.cloneSelectedElements(20,20); - }; - - var clickAlign = function() { - var letter = this.id.replace('tool_align','').charAt(0); - svgCanvas.alignSelectedElements(letter, $('#align_relative_to').val()); - }; - - var zoomImage = function(multiplier) { - var res = svgCanvas.getResolution(); - multiplier = multiplier?res.zoom * multiplier:1; - // setResolution(res.w * multiplier, res.h * multiplier, true); - $('#zoom').val(multiplier * 100); - svgCanvas.setZoom(multiplier); - zoomDone(); - updateCanvas(true); - }; - - var zoomDone = function() { - // updateBgImage(); - updateWireFrame(); - //updateCanvas(); // necessary? - } - - var clickWireframe = function() { - var wf = !$('#tool_wireframe').hasClass('push_button_pressed'); - if (wf) - $('#tool_wireframe').addClass('push_button_pressed').removeClass('tool_button'); - else - $('#tool_wireframe').removeClass('push_button_pressed').addClass('tool_button'); - workarea.toggleClass('wireframe'); - - if(supportsNonSS) return; - var wf_rules = $('#wireframe_rules'); - if(!wf_rules.length) { - wf_rules = $('<style id="wireframe_rules"><\/style>').appendTo('head'); - } else { - wf_rules.empty(); - } - - updateWireFrame(); - } - - var updateWireFrame = function() { - // Test support - if(supportsNonSS) return; - - var rule = "#workarea.wireframe #svgcontent * { stroke-width: " + 1/svgCanvas.getZoom() + "px; }"; - $('#wireframe_rules').text(workarea.hasClass('wireframe') ? rule : ""); - } - - var showSourceEditor = function(e, forSaving){ - if (editingsource) return; - editingsource = true; - - $('#save_output_btns').toggle(!!forSaving); - $('#tool_source_back').toggle(!forSaving); - - var str = orig_source = svgCanvas.getSvgString(); - $('#svg_source_textarea').val(str); - $('#svg_source_editor').fadeIn(); - properlySourceSizeTextArea(); - $('#svg_source_textarea').focus(); - }; - - $('#svg_docprops_container, #svg_prefs_container').draggable({cancel:'button,fieldset', containment: 'window'}); - - var showDocProperties = function(){ - if (docprops) return; - docprops = true; - - // This selects the correct radio button by using the array notation - $('#image_save_opts input').val([curPrefs.img_save]); - - // update resolution option with actual resolution - var res = svgCanvas.getResolution(); - if(curConfig.baseUnit !== "px") { - res.w = svgedit.units.convertUnit(res.w) + curConfig.baseUnit; - res.h = svgedit.units.convertUnit(res.h) + curConfig.baseUnit; - } - - $('#canvas_width').val(res.w); - $('#canvas_height').val(res.h); - $('#canvas_title').val(svgCanvas.getDocumentTitle()); - - $('#svg_docprops').show(); - }; - - - var showPreferences = function(){ - if (preferences) return; - preferences = true; - $('#main_menu').hide(); - - // Update background color with current one - var blocks = $('#bg_blocks div'); - var cur_bg = 'cur_background'; - var canvas_bg = $.pref('bkgd_color'); - var url = $.pref('bkgd_url'); - // if(url) url = url[1]; - blocks.each(function() { - var blk = $(this); - var is_bg = blk.css('background-color') == canvas_bg; - blk.toggleClass(cur_bg, is_bg); - if(is_bg) $('#canvas_bg_url').removeClass(cur_bg); - }); - if(!canvas_bg) blocks.eq(0).addClass(cur_bg); - if(url) { - $('#canvas_bg_url').val(url); - } - $('grid_snapping_step').attr('value', curConfig.snappingStep); - if (curConfig.gridSnapping == true) { - $('#grid_snapping_on').attr('checked', 'checked'); - } else { - $('#grid_snapping_on').removeAttr('checked'); - } - - $('#svg_prefs').show(); - }; - - var properlySourceSizeTextArea = function(){ - // TODO: remove magic numbers here and get values from CSS - var height = $('#svg_source_container').height() - 80; - $('#svg_source_textarea').css('height', height); - }; - - var saveSourceEditor = function(){ - if (!editingsource) return; - - var saveChanges = function() { - svgCanvas.clearSelection(); - hideSourceEditor(); - zoomImage(); - populateLayers(); - updateTitle(); - prepPaints(); - } - - if (!svgCanvas.setSvgString($('#svg_source_textarea').val())) { - $.confirm(uiStrings.notification.QerrorsRevertToSource, function(ok) { - if(!ok) return false; - saveChanges(); - }); - } else { - saveChanges(); - } - setSelectMode(); - }; - - var updateTitle = function(title) { - title = title || svgCanvas.getDocumentTitle(); - var new_title = orig_title + (title?': ' + title:''); - - // Remove title update with current context info, isn't really necessary -// if(cur_context) { -// new_title = new_title + cur_context; -// } - $('title:first').text(new_title); - } - - var saveDocProperties = function(){ - // set title - var new_title = $('#canvas_title').val(); - updateTitle(new_title); - svgCanvas.setDocumentTitle(new_title); - - // update resolution - var width = $('#canvas_width'), w = width.val(); - var height = $('#canvas_height'), h = height.val(); - - if(w != "fit" && !svgedit.units.isValidUnit('width', w)) { - $.alert(uiStrings.notification.invalidAttrValGiven); - width.parent().addClass('error'); - return false; - } - - width.parent().removeClass('error'); - - if(h != "fit" && !svgedit.units.isValidUnit('height', h)) { - $.alert(uiStrings.notification.invalidAttrValGiven); - height.parent().addClass('error'); - return false; - } - - height.parent().removeClass('error'); - - if(!svgCanvas.setResolution(w, h)) { - $.alert(uiStrings.notification.noContentToFitTo); - return false; - } - - // set image save option - curPrefs.img_save = $('#image_save_opts :checked').val(); - $.pref('img_save',curPrefs.img_save); - updateCanvas(); - hideDocProperties(); - }; - - var savePreferences = function() { - // set background - var color = $('#bg_blocks div.cur_background').css('background-color') || '#FFF'; - setBackground(color, $('#canvas_bg_url').val()); - - // set language - var lang = $('#lang_select').val(); - if(lang != curPrefs.lang) { - Editor.putLocale(lang); - } - - // set icon size - setIconSize($('#iconsize').val()); - - // set grid setting - curConfig.gridSnapping = $('#grid_snapping_on')[0].checked; - curConfig.snappingStep = $('#grid_snapping_step').val(); - curConfig.showRulers = $('#show_rulers')[0].checked; - - $('#rulers').toggle(curConfig.showRulers); - if(curConfig.showRulers) updateRulers(); - curConfig.baseUnit = $('#base_unit').val(); - - svgCanvas.setConfig(curConfig); - - updateCanvas(); - hidePreferences(); - } - - function setBackground(color, url) { -// if(color == curPrefs.bkgd_color && url == curPrefs.bkgd_url) return; - $.pref('bkgd_color', color); - $.pref('bkgd_url', url); - - // This should be done in svgcanvas.js for the borderRect fill - svgCanvas.setBackground(color, url); - } - - var setIcon = Editor.setIcon = function(elem, icon_id, forcedSize) { - var icon = (typeof icon_id === 'string') ? $.getSvgIcon(icon_id, true) : icon_id.clone(); - if(!icon) { - console.log('NOTE: Icon image missing: ' + icon_id); - return; - } - - $(elem).empty().append(icon); - } - - var ua_prefix; - (ua_prefix = function() { - var regex = /^(Moz|Webkit|Khtml|O|ms|Icab)(?=[A-Z])/; - var someScript = document.getElementsByTagName('script')[0]; - for(var prop in someScript.style) { - if(regex.test(prop)) { - // test is faster than match, so it's better to perform - // that on the lot and match only when necessary - return prop.match(regex)[0]; - } - } - - // Nothing found so far? - if('WebkitOpacity' in someScript.style) return 'Webkit'; - if('KhtmlOpacity' in someScript.style) return 'Khtml'; - - return ''; - }()); - - var scaleElements = function(elems, scale) { - var prefix = '-' + ua_prefix.toLowerCase() + '-'; - - var sides = ['top', 'left', 'bottom', 'right']; - - elems.each(function() { -// console.log('go', scale); - - // Handled in CSS - // this.style[ua_prefix + 'Transform'] = 'scale(' + scale + ')'; - - var el = $(this); - - var w = el.outerWidth() * (scale - 1); - var h = el.outerHeight() * (scale - 1); - var margins = {}; - - for(var i = 0; i < 4; i++) { - var s = sides[i]; - - var cur = el.data('orig_margin-' + s); - if(cur == null) { - cur = parseInt(el.css('margin-' + s)); - // Cache the original margin - el.data('orig_margin-' + s, cur); - } - var val = cur * scale; - if(s === 'right') { - val += w; - } else if(s === 'bottom') { - val += h; - } - - el.css('margin-' + s, val); -// el.css('outline', '1px solid red'); - } - }); - } - - var setIconSize = Editor.setIconSize = function(size, force) { - if(size == curPrefs.size && !force) return; -// return; -// var elems = $('.tool_button, .push_button, .tool_button_current, .disabled, .icon_label, #url_notice, #tool_open'); - console.log('size', size); - - var sel_toscale = '#tools_top .toolset, #editor_panel > *, #history_panel > *,\ - #main_button, #tools_left > *, #path_node_panel > *, #multiselected_panel > *,\ - #g_panel > *, #tool_font_size > *, .tools_flyout'; - - var elems = $(sel_toscale); - - var scale = 1; - - if(typeof size == 'number') { - scale = size; - } else { - var icon_sizes = { s:.75, m:1, l:1.25, xl:1.5 }; - scale = icon_sizes[size]; - } - - Editor.tool_scale = tool_scale = scale; - - setFlyoutPositions(); - // $('.tools_flyout').each(function() { -// var pos = $(this).position(); -// console.log($(this), pos.left+(34 * scale)); -// $(this).css({'left': pos.left+(34 * scale), 'top': pos.top+(77 * scale)}); -// console.log('l', $(this).css('left')); -// }); - -// var scale = .75;//0.75; - - var hidden_ps = elems.parents(':hidden'); - hidden_ps.css('visibility', 'hidden').show(); - scaleElements(elems, scale); - hidden_ps.css('visibility', 'visible').hide(); -// console.timeEnd('elems'); -// return; - - $.pref('iconsize', size); - $('#iconsize').val(size); - - - // Change icon size -// $('.tool_button, .push_button, .tool_button_current, .disabled, .icon_label, #url_notice, #tool_open') -// .find('> svg, > img').each(function() { -// this.setAttribute('width',size_num); -// this.setAttribute('height',size_num); -// }); -// -// $.resizeSvgIcons({ -// '.flyout_arrow_horiz > svg, .flyout_arrow_horiz > img': size_num / 5, -// '#logo > svg, #logo > img': size_num * 1.3, -// '#tools_bottom .icon_label > *': (size_num === 16 ? 18 : size_num * .75) -// }); -// if(size != 's') { -// $.resizeSvgIcons({'#layerbuttons svg, #layerbuttons img': size_num * .6}); -// } - - // Note that all rules will be prefixed with '#svg_editor' when parsed - var cssResizeRules = { -// ".tool_button,\ -// .push_button,\ -// .tool_button_current,\ -// .push_button_pressed,\ -// .disabled,\ -// .icon_label,\ -// .tools_flyout .tool_button": { -// 'width': {s: '16px', l: '32px', xl: '48px'}, -// 'height': {s: '16px', l: '32px', xl: '48px'}, -// 'padding': {s: '1px', l: '2px', xl: '3px'} -// }, -// ".tool_sep": { -// 'height': {s: '16px', l: '32px', xl: '48px'}, -// 'margin': {s: '2px 2px', l: '2px 5px', xl: '2px 8px'} -// }, -// "#main_icon": { -// 'width': {s: '31px', l: '53px', xl: '75px'}, -// 'height': {s: '22px', l: '42px', xl: '64px'} -// }, - "#tools_top": { - 'left': 50, - 'height': 72 - }, - "#tools_left": { - 'width': 31, - 'top': 74 - }, - "div#workarea": { - 'left': 38, - 'top': 74 - } -// "#tools_bottom": { -// 'left': {s: '27px', l: '46px', xl: '65px'}, -// 'height': {s: '58px', l: '98px', xl: '145px'} -// }, -// "#color_tools": { -// 'border-spacing': {s: '0 1px'}, -// 'margin-top': {s: '-1px'} -// }, -// "#color_tools .icon_label": { -// 'width': {l:'43px', xl: '60px'} -// }, -// ".color_tool": { -// 'height': {s: '20px'} -// }, -// "#tool_opacity": { -// 'top': {s: '1px'}, -// 'height': {s: 'auto', l:'auto', xl:'auto'} -// }, -// "#tools_top input, #tools_bottom input": { -// 'margin-top': {s: '2px', l: '4px', xl: '5px'}, -// 'height': {s: 'auto', l: 'auto', xl: 'auto'}, -// 'border': {s: '1px solid #555', l: 'auto', xl: 'auto'}, -// 'font-size': {s: '.9em', l: '1.2em', xl: '1.4em'} -// }, -// "#zoom_panel": { -// 'margin-top': {s: '3px', l: '4px', xl: '5px'} -// }, -// "#copyright, #tools_bottom .label": { -// 'font-size': {l: '1.5em', xl: '2em'}, -// 'line-height': {s: '15px'} -// }, -// "#tools_bottom_2": { -// 'width': {l: '295px', xl: '355px'}, -// 'top': {s: '4px'} -// }, -// "#tools_top > div, #tools_top": { -// 'line-height': {s: '17px', l: '34px', xl: '50px'} -// }, -// ".dropdown button": { -// 'height': {s: '18px', l: '34px', xl: '40px'}, -// 'line-height': {s: '18px', l: '34px', xl: '40px'}, -// 'margin-top': {s: '3px'} -// }, -// "#tools_top label, #tools_bottom label": { -// 'font-size': {s: '1em', l: '1.5em', xl: '2em'}, -// 'height': {s: '25px', l: '42px', xl: '64px'} -// }, -// "div.toolset": { -// 'height': {s: '25px', l: '42px', xl: '64px'} -// }, -// "#tool_bold, #tool_italic": { -// 'font-size': {s: '1.5em', l: '3em', xl: '4.5em'} -// }, -// "#sidepanels": { -// 'top': {s: '50px', l: '88px', xl: '125px'}, -// 'bottom': {s: '51px', l: '68px', xl: '65px'} -// }, -// '#layerbuttons': { -// 'width': {l: '130px', xl: '175px'}, -// 'height': {l: '24px', xl: '30px'} -// }, -// '#layerlist': { -// 'width': {l: '128px', xl: '150px'} -// }, -// '.layer_button': { -// 'width': {l: '19px', xl: '28px'}, -// 'height': {l: '19px', xl: '28px'} -// }, -// "input.spin-button": { -// 'background-image': {l: "url('images/spinbtn_updn_big.png')", xl: "url('images/spinbtn_updn_big.png')"}, -// 'background-position': {l: '100% -5px', xl: '100% -2px'}, -// 'padding-right': {l: '24px', xl: '24px' } -// }, -// "input.spin-button.up": { -// 'background-position': {l: '100% -45px', xl: '100% -42px'} -// }, -// "input.spin-button.down": { -// 'background-position': {l: '100% -85px', xl: '100% -82px'} -// }, -// "#position_opts": { -// 'width': {all: (size_num*4) +'px'} -// } - }; - - var rule_elem = $('#tool_size_rules'); - if(!rule_elem.length) { - rule_elem = $('<style id="tool_size_rules"><\/style>').appendTo('head'); - } else { - rule_elem.empty(); - } - - if(size != 'm') { - var style_str = ''; - $.each(cssResizeRules, function(selector, rules) { - selector = '#svg_editor ' + selector.replace(/,/g,', #svg_editor'); - style_str += selector + '{'; - $.each(rules, function(prop, values) { - if(typeof values === 'number') { - var val = (values * scale) + 'px'; - } else if(values[size] || values.all) { - var val = (values[size] || values.all); - } - style_str += (prop + ':' + val + ';'); - }); - style_str += '}'; - }); - //this.style[ua_prefix + 'Transform'] = 'scale(' + scale + ')'; - var prefix = '-' + ua_prefix.toLowerCase() + '-'; - style_str += (sel_toscale + '{' + prefix + 'transform: scale(' + scale + ');}' - + ' #svg_editor div.toolset .toolset {' + prefix + 'transform: scale(1); margin: 1px !important;}' // Hack for markers - + ' #svg_editor .ui-slider {' + prefix + 'transform: scale(' + (1/scale) + ');}' // Hack for sliders - ); - rule_elem.text(style_str); - } - - setFlyoutPositions(); - } - - var cancelOverlays = function() { - $('#dialog_box').hide(); - if (!editingsource && !docprops && !preferences) { - if(cur_context) { - svgCanvas.leaveContext(); - } - return; - }; - - if (editingsource) { - if (orig_source !== $('#svg_source_textarea').val()) { - $.confirm(uiStrings.notification.QignoreSourceChanges, function(ok) { - if(ok) hideSourceEditor(); - }); - } else { - hideSourceEditor(); - } - } - else if (docprops) { - hideDocProperties(); - } else if (preferences) { - hidePreferences(); - } - resetScrollPos(); - }; - - var hideSourceEditor = function(){ - $('#svg_source_editor').hide(); - editingsource = false; - $('#svg_source_textarea').blur(); - }; - - var hideDocProperties = function(){ - $('#svg_docprops').hide(); - $('#canvas_width,#canvas_height').removeAttr('disabled'); - $('#resolution')[0].selectedIndex = 0; - $('#image_save_opts input').val([curPrefs.img_save]); - docprops = false; - }; - - var hidePreferences = function(){ - $('#svg_prefs').hide(); - preferences = false; - }; - - var win_wh = {width:$(window).width(), height:$(window).height()}; - - var resetScrollPos = $.noop, curScrollPos; - - // Fix for Issue 781: Drawing area jumps to top-left corner on window resize (IE9) - if(svgedit.browser.isIE()) { - (function() { - resetScrollPos = function() { - if(workarea[0].scrollLeft === 0 - && workarea[0].scrollTop === 0) { - workarea[0].scrollLeft = curScrollPos.left; - workarea[0].scrollTop = curScrollPos.top; - } - } - - curScrollPos = { - left: workarea[0].scrollLeft, - top: workarea[0].scrollTop - }; - - $(window).resize(resetScrollPos); - svgEditor.ready(function() { - // TODO: Find better way to detect when to do this to minimize - // flickering effect - setTimeout(function() { - resetScrollPos(); - }, 500); - }); - - workarea.scroll(function() { - curScrollPos = { - left: workarea[0].scrollLeft, - top: workarea[0].scrollTop - }; - }); - }()); - } - - $(window).resize(function(evt) { - if (editingsource) { - properlySourceSizeTextArea(); - } - - $.each(win_wh, function(type, val) { - var curval = $(window)[type](); - workarea[0]['scroll' + (type==='width'?'Left':'Top')] -= (curval - val)/2; - win_wh[type] = curval; - }); - }); - - (function() { - workarea.scroll(function() { - // TODO: jQuery's scrollLeft/Top() wouldn't require a null check - if ($('#ruler_x').length != 0) { - $('#ruler_x')[0].scrollLeft = workarea[0].scrollLeft; - } - if ($('#ruler_y').length != 0) { - $('#ruler_y')[0].scrollTop = workarea[0].scrollTop; - } - }); - - }()); - - $('#url_notice').click(function() { - $.alert(this.title); - }); - - $('#change_image_url').click(promptImgURL); - - function promptImgURL() { - var curhref = svgCanvas.getHref(selectedElement); - curhref = curhref.indexOf("data:") === 0?"":curhref; - $.prompt(uiStrings.notification.enterNewImgURL, curhref, function(url) { - if(url) setImageURL(url); - }); - } - - // added these event handlers for all the push buttons so they - // behave more like buttons being pressed-in and not images - (function() { - var toolnames = ['clear','open','save','source','delete','delete_multi','paste','clone','clone_multi','move_top','move_bottom']; - var all_tools = ''; - var cur_class = 'tool_button_current'; - - $.each(toolnames, function(i,item) { - all_tools += '#tool_' + item + (i==toolnames.length-1?',':''); - }); - - $(all_tools).mousedown(function() { - $(this).addClass(cur_class); - }).bind('mousedown mouseout', function() { - $(this).removeClass(cur_class); - }); - - $('#tool_undo, #tool_redo').mousedown(function(){ - if (!$(this).hasClass('disabled')) $(this).addClass(cur_class); - }).bind('mousedown mouseout',function(){ - $(this).removeClass(cur_class);} - ); - }()); - - // switch modifier key in tooltips if mac - // NOTE: This code is not used yet until I can figure out how to successfully bind ctrl/meta - // in Opera and Chrome - if (isMac && !window.opera) { - var shortcutButtons = ["tool_clear", "tool_save", "tool_source", "tool_undo", "tool_redo", "tool_clone"]; - var i = shortcutButtons.length; - while (i--) { - var button = document.getElementById(shortcutButtons[i]); - if (button != null) { - var title = button.title; - var index = title.indexOf("Ctrl+"); - button.title = [title.substr(0, index), "Cmd+", title.substr(index + 5)].join(''); - } - } - } - - // TODO: go back to the color boxes having white background-color and then setting - // background-image to none.png (otherwise partially transparent gradients look weird) - var colorPicker = function(elem) { - var picker = elem.attr('id') == 'stroke_color' ? 'stroke' : 'fill'; -// var opacity = (picker == 'stroke' ? $('#stroke_opacity') : $('#fill_opacity')); - var paint = paintBox[picker].paint; - var title = (picker == 'stroke' ? 'Pick a Stroke Paint and Opacity' : 'Pick a Fill Paint and Opacity'); - var was_none = false; - var pos = elem.offset(); - $("#color_picker") - .draggable({cancel:'.jGraduate_tabs, .jGraduate_colPick, .jGraduate_gradPick, .jPicker', containment: 'window'}) - .css(curConfig.colorPickerCSS || {'left': pos.left-140, 'bottom': 40}) - .jGraduate( - { - paint: paint, - window: { pickerTitle: title }, - images: { clientPath: curConfig.jGraduatePath }, - newstop: 'inverse' - }, - function(p) { - paint = new $.jGraduate.Paint(p); - paintBox[picker].setPaint(paint); - svgCanvas.setPaint(picker, paint); - - $('#color_picker').hide(); - }, - function(p) { - $('#color_picker').hide(); - }); - }; - - var updateToolButtonState = function() { - var bNoFill = (svgCanvas.getColor('fill') == 'none'); - var bNoStroke = (svgCanvas.getColor('stroke') == 'none'); - var buttonsNeedingStroke = [ '#tool_fhpath', '#tool_line' ]; - var buttonsNeedingFillAndStroke = [ '#tools_rect .tool_button', '#tools_ellipse .tool_button', '#tool_text', '#tool_path']; - // if (bNoStroke) { - // for (var index in buttonsNeedingStroke) { - // var button = buttonsNeedingStroke[index]; - // if ($(button).hasClass('tool_button_current')) { - // clickSelect(); - // } - // $(button).addClass('disabled'); - // } - // } - // else { - // for (var index in buttonsNeedingStroke) { - // var button = buttonsNeedingStroke[index]; - // $(button).removeClass('disabled'); - // } - // } - - if (bNoStroke && bNoFill) { - for (var index in buttonsNeedingFillAndStroke) { - var button = buttonsNeedingFillAndStroke[index]; - if ($(button).hasClass('tool_button_current')) { - clickSelect(); - } - $(button).addClass('disabled'); - } - } - else { - for (var index in buttonsNeedingFillAndStroke) { - var button = buttonsNeedingFillAndStroke[index]; - $(button).removeClass('disabled'); - } - } - - svgCanvas.runExtensions("toolButtonStateUpdate", { - nofill: bNoFill, - nostroke: bNoStroke - }); - - // Disable flyouts if all inside are disabled - $('.tools_flyout').each(function() { - var shower = $('#' + this.id + '_show'); - var has_enabled = false; - $(this).children().each(function() { - if(!$(this).hasClass('disabled')) { - has_enabled = true; - } - }); - shower.toggleClass('disabled', !has_enabled); - }); - - operaRepaint(); - }; - - - - var PaintBox = function(container, type) { - var cur = curConfig[type === 'fill' ? 'initFill' : 'initStroke']; - - // set up gradients to be used for the buttons - var svgdocbox = new DOMParser().parseFromString( - '<svg xmlns="http://www.w3.org/2000/svg"><rect width="16.5" height="16.5"\ - fill="#' + cur.color + '" opacity="' + cur.opacity + '"/>\ - <defs><linearGradient id="gradbox_"/></defs></svg>', 'text/xml'); - var docElem = svgdocbox.documentElement; - - docElem = $(container)[0].appendChild(document.importNode(docElem, true)); - - docElem.setAttribute('width',16.5); - - this.rect = docElem.firstChild; - this.defs = docElem.getElementsByTagName('defs')[0]; - this.grad = this.defs.firstChild; - this.paint = new $.jGraduate.Paint({solidColor: cur.color}); - this.type = type; - - this.setPaint = function(paint, apply) { - this.paint = paint; - - var fillAttr = "none"; - var ptype = paint.type; - var opac = paint.alpha / 100; - - switch ( ptype ) { - case 'solidColor': - fillAttr = (paint[ptype] != 'none') ? "#" + paint[ptype] : paint[ptype]; - break; - case 'linearGradient': - case 'radialGradient': - this.defs.removeChild(this.grad); - this.grad = this.defs.appendChild(paint[ptype]); - var id = this.grad.id = 'gradbox_' + this.type; - fillAttr = "url(#" + id + ')'; - } - - this.rect.setAttribute('fill', fillAttr); - this.rect.setAttribute('opacity', opac); - - if(apply) { - svgCanvas.setColor(this.type, paintColor, true); - svgCanvas.setPaintOpacity(this.type, paintOpacity, true); - } - } - - this.update = function(apply) { - if(!selectedElement) return; - var type = this.type; - - switch ( selectedElement.tagName ) { - case 'use': - case 'image': - case 'foreignObject': - // These elements don't have fill or stroke, so don't change - // the current value - return; - case 'g': - case 'a': - var gPaint = null; - - var childs = selectedElement.getElementsByTagName('*'); - for(var i = 0, len = childs.length; i < len; i++) { - var elem = childs[i]; - var p = elem.getAttribute(type); - if(i === 0) { - gPaint = p; - } else if(gPaint !== p) { - gPaint = null; - break; - } - } - if(gPaint === null) { - // No common color, don't update anything - var paintColor = null; - return; - } - var paintColor = gPaint; - - var paintOpacity = 1; - break; - default: - var paintOpacity = parseFloat(selectedElement.getAttribute(type + "-opacity")); - if (isNaN(paintOpacity)) { - paintOpacity = 1.0; - } - - var defColor = type === "fill" ? "black" : "none"; - var paintColor = selectedElement.getAttribute(type) || defColor; - } - - if(apply) { - svgCanvas.setColor(type, paintColor, true); - svgCanvas.setPaintOpacity(type, paintOpacity, true); - } - - paintOpacity *= 100; - - var paint = getPaint(paintColor, paintOpacity, type); - // update the rect inside #fill_color/#stroke_color - this.setPaint(paint); - } - - this.prep = function() { - var ptype = this.paint.type; - - switch ( ptype ) { - case 'linearGradient': - case 'radialGradient': - var paint = new $.jGraduate.Paint({copy: this.paint}); - svgCanvas.setPaint(type, paint); - } - } - }; - - paintBox.fill = new PaintBox('#fill_color', 'fill'); - paintBox.stroke = new PaintBox('#stroke_color', 'stroke'); - - $('#stroke_width').val(curConfig.initStroke.width); - $('#group_opacity').val(curConfig.initOpacity * 100); - - // Use this SVG elem to test vectorEffect support - var test_el = paintBox.fill.rect.cloneNode(false); - test_el.setAttribute('style','vector-effect:non-scaling-stroke'); - var supportsNonSS = (test_el.style.vectorEffect === 'non-scaling-stroke'); - test_el.removeAttribute('style'); - var svgdocbox = paintBox.fill.rect.ownerDocument; - // Use this to test support for blur element. Seems to work to test support in Webkit - var blur_test = svgdocbox.createElementNS('http://www.w3.org/2000/svg', 'feGaussianBlur'); - if(typeof blur_test.stdDeviationX === "undefined") { - $('#tool_blur').hide(); - } - $(blur_test).remove(); - - // Test for zoom icon support - (function() { - var pre = '-' + ua_prefix.toLowerCase() + '-zoom-'; - var zoom = pre + 'in'; - workarea.css('cursor', zoom); - if(workarea.css('cursor') === zoom) { - zoomInIcon = zoom; - zoomOutIcon = pre + 'out'; - } - workarea.css('cursor', 'auto'); - }()); - - - - // Test for embedImage support (use timeout to not interfere with page load) - setTimeout(function() { - svgCanvas.embedImage('images/logo.png', function(datauri) { - if(!datauri) { - // Disable option - $('#image_save_opts [value=embed]').attr('disabled','disabled'); - $('#image_save_opts input').val(['ref']); - curPrefs.img_save = 'ref'; - $('#image_opt_embed').css('color','#666').attr('title',uiStrings.notification.featNotSupported); - } - }); - },1000); - - $('#fill_color, #tool_fill .icon_label').click(function(){ - colorPicker($('#fill_color')); - updateToolButtonState(); - }); - - $('#stroke_color, #tool_stroke .icon_label').click(function(){ - colorPicker($('#stroke_color')); - updateToolButtonState(); - }); - - $('#group_opacityLabel').click(function() { - $('#opacity_dropdown button').mousedown(); - $(window).mouseup(); - }); - - $('#zoomLabel').click(function() { - $('#zoom_dropdown button').mousedown(); - $(window).mouseup(); - }); - - $('#tool_move_top').mousedown(function(evt){ - $('#tools_stacking').show(); - evt.preventDefault(); - }); - - $('.layer_button').mousedown(function() { - $(this).addClass('layer_buttonpressed'); - }).mouseout(function() { - $(this).removeClass('layer_buttonpressed'); - }).mouseup(function() { - $(this).removeClass('layer_buttonpressed'); - }); - - $('.push_button').mousedown(function() { - if (!$(this).hasClass('disabled')) { - $(this).addClass('push_button_pressed').removeClass('push_button'); - } - }).mouseout(function() { - $(this).removeClass('push_button_pressed').addClass('push_button'); - }).mouseup(function() { - $(this).removeClass('push_button_pressed').addClass('push_button'); - }); - - $('#layer_new').click(function() { - var i = svgCanvas.getCurrentDrawing().getNumLayers(); - do { - var uniqName = uiStrings.layers.layer + " " + ++i; - } while(svgCanvas.getCurrentDrawing().hasLayer(uniqName)); - - $.prompt(uiStrings.notification.enterUniqueLayerName,uniqName, function(newName) { - if (!newName) return; - if (svgCanvas.getCurrentDrawing().hasLayer(newName)) { - $.alert(uiStrings.notification.dupeLayerName); - return; - } - svgCanvas.createLayer(newName); - updateContextPanel(); - populateLayers(); - }); - }); - - function deleteLayer() { - if (svgCanvas.deleteCurrentLayer()) { - updateContextPanel(); - populateLayers(); - // This matches what SvgCanvas does - // TODO: make this behavior less brittle (svg-editor should get which - // layer is selected from the canvas and then select that one in the UI) - $('#layerlist tr.layer').removeClass("layersel"); - $('#layerlist tr.layer:first').addClass("layersel"); - } - } - - function cloneLayer() { - var name = svgCanvas.getCurrentDrawing().getCurrentLayerName() + ' copy'; - - $.prompt(uiStrings.notification.enterUniqueLayerName, name, function(newName) { - if (!newName) return; - if (svgCanvas.getCurrentDrawing().hasLayer(newName)) { - $.alert(uiStrings.notification.dupeLayerName); - return; - } - svgCanvas.cloneLayer(newName); - updateContextPanel(); - populateLayers(); - }); - } - - function mergeLayer() { - if($('#layerlist tr.layersel').index() == svgCanvas.getCurrentDrawing().getNumLayers()-1) return; - svgCanvas.mergeLayer(); - updateContextPanel(); - populateLayers(); - } - - function moveLayer(pos) { - var curIndex = $('#layerlist tr.layersel').index(); - var total = svgCanvas.getCurrentDrawing().getNumLayers(); - if(curIndex > 0 || curIndex < total-1) { - curIndex += pos; - svgCanvas.setCurrentLayerPosition(total-curIndex-1); - populateLayers(); - } - } - - $('#layer_delete').click(deleteLayer); - - $('#layer_up').click(function() { - moveLayer(-1); - }); - - $('#layer_down').click(function() { - moveLayer(1); - }); - - $('#layer_rename').click(function() { - var curIndex = $('#layerlist tr.layersel').prevAll().length; - var oldName = $('#layerlist tr.layersel td.layername').text(); - $.prompt(uiStrings.notification.enterNewLayerName,"", function(newName) { - if (!newName) return; - if (oldName == newName || svgCanvas.getCurrentDrawing().hasLayer(newName)) { - $.alert(uiStrings.notification.layerHasThatName); - return; - } - - svgCanvas.renameCurrentLayer(newName); - populateLayers(); - }); - }); - - var SIDEPANEL_MAXWIDTH = 300; - var SIDEPANEL_OPENWIDTH = 150; - var sidedrag = -1, sidedragging = false, allowmove = false; - - var resizePanel = function(evt) { - if (!allowmove) return; - if (sidedrag == -1) return; - sidedragging = true; - var deltax = sidedrag - evt.pageX; - - var sidepanels = $('#sidepanels'); - var sidewidth = parseInt(sidepanels.css('width')); - if (sidewidth+deltax > SIDEPANEL_MAXWIDTH) { - deltax = SIDEPANEL_MAXWIDTH - sidewidth; - sidewidth = SIDEPANEL_MAXWIDTH; - } - else if (sidewidth+deltax < 2) { - deltax = 2 - sidewidth; - sidewidth = 2; - } - - if (deltax == 0) return; - sidedrag -= deltax; - - var layerpanel = $('#layerpanel'); - workarea.css('right', parseInt(workarea.css('right'))+deltax); - sidepanels.css('width', parseInt(sidepanels.css('width'))+deltax); - layerpanel.css('width', parseInt(layerpanel.css('width'))+deltax); - var ruler_x = $('#ruler_x'); - ruler_x.css('right', parseInt(ruler_x.css('right')) + deltax); - } - - $('#sidepanel_handle') - .mousedown(function(evt) { - sidedrag = evt.pageX; - $(window).mousemove(resizePanel); - allowmove = false; - // Silly hack for Chrome, which always runs mousemove right after mousedown - setTimeout(function() { - allowmove = true; - }, 20); - }) - .mouseup(function(evt) { - if (!sidedragging) toggleSidePanel(); - sidedrag = -1; - sidedragging = false; - }); - - $(window).mouseup(function() { - sidedrag = -1; - sidedragging = false; - $('#svg_editor').unbind('mousemove', resizePanel); - }); - - // if width is non-zero, then fully close it, otherwise fully open it - // the optional close argument forces the side panel closed - var toggleSidePanel = function(close){ - var w = parseInt($('#sidepanels').css('width')); - var deltax = (w > 2 || close ? 2 : SIDEPANEL_OPENWIDTH) - w; - var sidepanels = $('#sidepanels'); - var layerpanel = $('#layerpanel'); - var ruler_x = $('#ruler_x'); - workarea.css('right', parseInt(workarea.css('right')) + deltax); - sidepanels.css('width', parseInt(sidepanels.css('width')) + deltax); - layerpanel.css('width', parseInt(layerpanel.css('width')) + deltax); - ruler_x.css('right', parseInt(ruler_x.css('right')) + deltax); - }; - - // this function highlights the layer passed in (by fading out the other layers) - // if no layer is passed in, this function restores the other layers - var toggleHighlightLayer = function(layerNameToHighlight) { - var curNames = new Array(svgCanvas.getCurrentDrawing().getNumLayers()); - for (var i = 0; i < curNames.length; ++i) { curNames[i] = svgCanvas.getCurrentDrawing().getLayerName(i); } - - if (layerNameToHighlight) { - for (var i = 0; i < curNames.length; ++i) { - if (curNames[i] != layerNameToHighlight) { - svgCanvas.getCurrentDrawing().setLayerOpacity(curNames[i], 0.5); - } - } - } - else { - for (var i = 0; i < curNames.length; ++i) { - svgCanvas.getCurrentDrawing().setLayerOpacity(curNames[i], 1.0); - } - } - }; - - var populateLayers = function(){ - var layerlist = $('#layerlist tbody'); - var selLayerNames = $('#selLayerNames'); - layerlist.empty(); - selLayerNames.empty(); - var currentLayerName = svgCanvas.getCurrentDrawing().getCurrentLayerName(); - var layer = svgCanvas.getCurrentDrawing().getNumLayers(); - var icon = $.getSvgIcon('eye'); - // we get the layers in the reverse z-order (the layer rendered on top is listed first) - while (layer--) { - var name = svgCanvas.getCurrentDrawing().getLayerName(layer); - // contenteditable=\"true\" - var appendstr = "<tr class=\"layer"; - if (name == currentLayerName) { - appendstr += " layersel" - } - appendstr += "\">"; - - if (svgCanvas.getCurrentDrawing().getLayerVisibility(name)) { - appendstr += "<td class=\"layervis\"/><td class=\"layername\" >" + name + "</td></tr>"; - } - else { - appendstr += "<td class=\"layervis layerinvis\"/><td class=\"layername\" >" + name + "</td></tr>"; - } - layerlist.append(appendstr); - selLayerNames.append("<option value=\"" + name + "\">" + name + "</option>"); - } - if(icon !== undefined) { - var copy = icon.clone(); - $('td.layervis',layerlist).append(icon.clone()); - $.resizeSvgIcons({'td.layervis .svg_icon':14}); - } - // handle selection of layer - $('#layerlist td.layername') - .mouseup(function(evt){ - $('#layerlist tr.layer').removeClass("layersel"); - var row = $(this.parentNode); - row.addClass("layersel"); - svgCanvas.setCurrentLayer(this.textContent); - evt.preventDefault(); - }) - .mouseover(function(evt){ - $(this).css({"font-style": "italic", "color":"blue"}); - toggleHighlightLayer(this.textContent); - }) - .mouseout(function(evt){ - $(this).css({"font-style": "normal", "color":"black"}); - toggleHighlightLayer(); - }); - $('#layerlist td.layervis').click(function(evt){ - var row = $(this.parentNode).prevAll().length; - var name = $('#layerlist tr.layer:eq(' + row + ') td.layername').text(); - var vis = $(this).hasClass('layerinvis'); - svgCanvas.setLayerVisibility(name, vis); - if (vis) { - $(this).removeClass('layerinvis'); - } - else { - $(this).addClass('layerinvis'); - } - }); - - // if there were too few rows, let's add a few to make it not so lonely - var num = 5 - $('#layerlist tr.layer').size(); - while (num-- > 0) { - // FIXME: there must a better way to do this - layerlist.append("<tr><td style=\"color:white\">_</td><td/></tr>"); - } - }; - populateLayers(); - - // function changeResolution(x,y) { - // var zoom = svgCanvas.getResolution().zoom; - // setResolution(x * zoom, y * zoom); - // } - - var centerCanvas = function() { - // this centers the canvas vertically in the workarea (horizontal handled in CSS) - workarea.css('line-height', workarea.height() + 'px'); - }; - - $(window).bind('load resize', centerCanvas); - - function stepFontSize(elem, step) { - var orig_val = elem.value-0; - var sug_val = orig_val + step; - var increasing = sug_val >= orig_val; - if(step === 0) return orig_val; - - if(orig_val >= 24) { - if(increasing) { - return Math.round(orig_val * 1.1); - } else { - return Math.round(orig_val / 1.1); - } - } else if(orig_val <= 1) { - if(increasing) { - return orig_val * 2; - } else { - return orig_val / 2; - } - } else { - return sug_val; - } - } - - function stepZoom(elem, step) { - var orig_val = elem.value-0; - if(orig_val === 0) return 100; - var sug_val = orig_val + step; - if(step === 0) return orig_val; - - if(orig_val >= 100) { - return sug_val; - } else { - if(sug_val >= orig_val) { - return orig_val * 2; - } else { - return orig_val / 2; - } - } - } - - // function setResolution(w, h, center) { - // updateCanvas(); - // // w-=0; h-=0; - // // $('#svgcanvas').css( { 'width': w, 'height': h } ); - // // $('#canvas_width').val(w); - // // $('#canvas_height').val(h); - // // - // // if(center) { - // // var w_area = workarea; - // // var scroll_y = h/2 - w_area.height()/2; - // // var scroll_x = w/2 - w_area.width()/2; - // // w_area[0].scrollTop = scroll_y; - // // w_area[0].scrollLeft = scroll_x; - // // } - // } - - $('#resolution').change(function(){ - var wh = $('#canvas_width,#canvas_height'); - if(!this.selectedIndex) { - if($('#canvas_width').val() == 'fit') { - wh.removeAttr("disabled").val(100); - } - } else if(this.value == 'content') { - wh.val('fit').attr("disabled","disabled"); - } else { - var dims = this.value.split('x'); - $('#canvas_width').val(dims[0]); - $('#canvas_height').val(dims[1]); - wh.removeAttr("disabled"); - } - }); - - //Prevent browser from erroneously repopulating fields - $('input,select').attr("autocomplete","off"); - - // Associate all button actions as well as non-button keyboard shortcuts - var Actions = function() { - // sel:'selector', fn:function, evt:'event', key:[key, preventDefault, NoDisableInInput] - var tool_buttons = [ - {sel:'#tool_select', fn: clickSelect, evt: 'click', key: ['S', true]}, - {sel:'#tool_fhpath', fn: clickFHPath, evt: 'click', key: ['L', true]}, - {sel:'#tool_line', fn: clickLine, evt: 'click', key: ['V', true]}, - {sel:'#tool_rect', fn: clickRect, evt: 'mouseup', key: ['R', true], parent: '#tools_rect', icon: 'rect'}, - {sel:'#tool_square', fn: clickSquare, evt: 'mouseup', parent: '#tools_rect', icon: 'square'}, - {sel:'#tool_fhrect', fn: clickFHRect, evt: 'mouseup', parent: '#tools_rect', icon: 'fh_rect'}, - {sel:'#tool_ellipse', fn: clickEllipse, evt: 'mouseup', key: ['E', true], parent: '#tools_ellipse', icon: 'ellipse'}, - {sel:'#tool_circle', fn: clickCircle, evt: 'mouseup', parent: '#tools_ellipse', icon: 'circle'}, - {sel:'#tool_fhellipse', fn: clickFHEllipse, evt: 'mouseup', parent: '#tools_ellipse', icon: 'fh_ellipse'}, - {sel:'#tool_path', fn: clickPath, evt: 'click', key: ['P', true]}, - {sel:'#tool_text', fn: clickText, evt: 'click', key: ['T', true]}, - {sel:'#tool_image', fn: clickImage, evt: 'mouseup'}, - {sel:'#tool_zoom', fn: clickZoom, evt: 'mouseup', key: ['', true]}, - {sel:'#tool_clear', fn: clickClear, evt: 'mouseup', key: ['N', true]}, - {sel:'#tool_save', fn: function() { editingsource?saveSourceEditor():clickSave()}, evt: 'mouseup', key: ['', true]}, - {sel:'#tool_export', fn: clickExport, evt: 'mouseup'}, - {sel:'#tool_open', fn: clickOpen, evt: 'mouseup', key: ['O', true]}, - {sel:'#tool_import', fn: clickImport, evt: 'mouseup'}, - {sel:'#tool_source', fn: showSourceEditor, evt: 'click', key: ['U', true]}, - {sel:'#tool_wireframe', fn: clickWireframe, evt: 'click', key: ['W', true]}, - {sel:'#tool_source_cancel,#svg_source_overlay,#tool_docprops_cancel,#tool_prefs_cancel', fn: cancelOverlays, evt: 'click', key: ['esc', false, false], hidekey: true}, - {sel:'#tool_source_save', fn: saveSourceEditor, evt: 'click'}, - {sel:'#tool_docprops_save', fn: saveDocProperties, evt: 'click'}, - {sel:'#tool_docprops', fn: showDocProperties, evt: 'mouseup'}, - {sel:'#tool_prefs_save', fn: savePreferences, evt: 'click'}, - {sel:'#tool_prefs_option', fn: function() {showPreferences();return false}, evt: 'mouseup'}, - {sel:'#tool_delete,#tool_delete_multi', fn: deleteSelected, evt: 'click', key: ['del/backspace', true]}, - {sel:'#tool_reorient', fn: reorientPath, evt: 'click'}, - {sel:'#tool_node_link', fn: linkControlPoints, evt: 'click'}, - {sel:'#tool_node_clone', fn: clonePathNode, evt: 'click'}, - {sel:'#tool_node_delete', fn: deletePathNode, evt: 'click'}, - {sel:'#tool_openclose_path', fn: opencloseSubPath, evt: 'click'}, - {sel:'#tool_add_subpath', fn: addSubPath, evt: 'click'}, - {sel:'#tool_move_top', fn: moveToTopSelected, evt: 'click', key: 'ctrl+shift+]'}, - {sel:'#tool_move_bottom', fn: moveToBottomSelected, evt: 'click', key: 'ctrl+shift+['}, - {sel:'#tool_topath', fn: convertToPath, evt: 'click'}, - {sel:'#tool_make_link,#tool_make_link_multi', fn: makeHyperlink, evt: 'click'}, - {sel:'#tool_undo', fn: clickUndo, evt: 'click', key: [modKey+'z', true]}, - {sel:'#tool_redo', fn: clickRedo, evt: 'click', key: [modKey+'y', true]}, - {sel:'#tool_clone,#tool_clone_multi', fn: clickClone, evt: 'click', key: ['D', true]}, - {sel:'#tool_group', fn: clickGroup, evt: 'click', key: ['G', true]}, - {sel:'#tool_ungroup', fn: clickGroup, evt: 'click'}, - {sel:'#tool_unlink_use', fn: clickGroup, evt: 'click'}, - {sel:'[id^=tool_align]', fn: clickAlign, evt: 'click'}, - // these two lines are required to make Opera work properly with the flyout mechanism - // {sel:'#tools_rect_show', fn: clickRect, evt: 'click'}, - // {sel:'#tools_ellipse_show', fn: clickEllipse, evt: 'click'}, - {sel:'#tool_bold', fn: clickBold, evt: 'mousedown'}, - {sel:'#tool_italic', fn: clickItalic, evt: 'mousedown'}, - {sel:'#sidepanel_handle', fn: toggleSidePanel, key: ['X']}, - {sel:'#copy_save_done', fn: cancelOverlays, evt: 'click'}, - - // Shortcuts not associated with buttons - - {key: 'ctrl+left', fn: function(){rotateSelected(0,1)}}, - {key: 'ctrl+right', fn: function(){rotateSelected(1,1)}}, - {key: 'ctrl+shift+left', fn: function(){rotateSelected(0,5)}}, - {key: 'ctrl+shift+right', fn: function(){rotateSelected(1,5)}}, - {key: 'shift+O', fn: selectPrev}, - {key: 'shift+P', fn: selectNext}, - {key: 'plus', fn: function(){zoomImage(1.25);}}, - {key: 'minus', fn: function(){zoomImage(.75);}}, - {key: '0', fn: function(){svgCanvas.zoomChanged('', 'canvas');}}, - {key: [modKey+']', true], fn: function(){moveUpDownSelected('Up');}}, - {key: [modKey+'[', true], fn: function(){moveUpDownSelected('Down');}}, - {key: ['up', true], fn: function(){moveSelected(0,-1);}}, - {key: ['down', true], fn: function(){moveSelected(0,1);}}, - {key: ['left', true], fn: function(){moveSelected(-1,0);}}, - {key: ['right', true], fn: function(){moveSelected(1,0);}}, - {key: 'shift+up', fn: function(){moveSelected(0,-10)}}, - {key: 'shift+down', fn: function(){moveSelected(0,10)}}, - {key: 'shift+left', fn: function(){moveSelected(-10,0)}}, - {key: 'shift+right', fn: function(){moveSelected(10,0)}}, - {key: ['alt+up', true], fn: function(){svgCanvas.cloneSelectedElements(0,-1)}}, - {key: ['alt+down', true], fn: function(){svgCanvas.cloneSelectedElements(0,1)}}, - {key: ['alt+left', true], fn: function(){svgCanvas.cloneSelectedElements(-1,0)}}, - {key: ['alt+right', true], fn: function(){svgCanvas.cloneSelectedElements(1,0)}}, - {key: ['alt+shift+up', true], fn: function(){svgCanvas.cloneSelectedElements(0,-10)}}, - {key: ['alt+shift+down', true], fn: function(){svgCanvas.cloneSelectedElements(0,10)}}, - {key: ['alt+shift+left', true], fn: function(){svgCanvas.cloneSelectedElements(-10,0)}}, - {key: ['alt+shift+right', true], fn: function(){svgCanvas.cloneSelectedElements(10,0)}}, - {key: 'A', fn: function(){svgCanvas.selectAllInCurrentLayer();}}, - - // Standard shortcuts - {key: modKey + 'shift+z', fn: clickRedo}, - {key: modKey+'x', fn: cutSelected}, - {key: modKey+'c', fn: copySelected}, - {key: modKey+'v', fn: pasteInCenter}, - - // Additional shortcuts - {key: ['esc', true, true], fn: svgCanvas.clearSelection}, - {key: 'ctrl+shift+L', fn: function(){$('#layerlist td.layername:contains("Labels"):last').trigger("mouseup")}}, - {key: 'ctrl+shift+M', fn: function(){$('#layerlist td.layername:contains("Masks"):last').trigger("mouseup")}} - - ]; - - // Tooltips not directly associated with a single function - var key_assocs = { - '4/Shift+4': '#tools_rect_show', - '5/Shift+5': '#tools_ellipse_show' - }; - - return { - setAll: function() { - var flyouts = {}; - - $.each(tool_buttons, function(i, opts) { - // Bind function to button - if(opts.sel) { - var btn = $(opts.sel); - if (btn.length == 0) return true; // Skip if markup does not exist - if(opts.evt) { - if (svgedit.browser.isTouch() && opts.evt === "click") opts.evt = "mousedown" - btn[opts.evt](opts.fn); - } - - // Add to parent flyout menu, if able to be displayed - if(opts.parent && $(opts.parent + '_show').length != 0) { - var f_h = $(opts.parent); - if(!f_h.length) { - f_h = makeFlyoutHolder(opts.parent.substr(1)); - } - - f_h.append(btn); - - if(!$.isArray(flyouts[opts.parent])) { - flyouts[opts.parent] = []; - } - flyouts[opts.parent].push(opts); - } - } - - - // Bind function to shortcut key - if(opts.key) { - // Set shortcut based on options - var keyval, shortcut = '', disInInp = true, fn = opts.fn, pd = false; - if($.isArray(opts.key)) { - keyval = opts.key[0]; - if(opts.key.length > 1) pd = opts.key[1]; - if(opts.key.length > 2) disInInp = opts.key[2]; - } else { - keyval = opts.key; - } - keyval += ''; - - $.each(keyval.split('/'), function(i, key) { - $(document).bind('keydown', key, function(e) { - fn(); - if(pd) { - e.preventDefault(); - } - // Prevent default on ALL keys? - return false; - }); - }); - - // Put shortcut in title - if(opts.sel && !opts.hidekey && btn.attr('title')) { - var new_title = btn.attr('title').split('[')[0] + ' (' + keyval + ')'; - key_assocs[keyval] = opts.sel; - // Disregard for menu items - if(!btn.parents('#main_menu').length) { - btn.attr('title', new_title); - } - } - } - }); - - // Setup flyouts - setupFlyouts(flyouts); - - - // Misc additional actions - - // Make "return" keypress trigger the change event - $('.attr_changer, #image_url').bind('keydown', 'return', - function(evt) {$(this).change();evt.preventDefault();} - ); - - $(window).bind('keydown', 'tab', function(e) { - if(ui_context === 'canvas') { - e.preventDefault(); - selectNext(); - } - }).bind('keydown', 'shift+tab', function(e) { - if(ui_context === 'canvas') { - e.preventDefault(); - selectPrev(); - } - }); - - $('#tool_zoom').dblclick(dblclickZoom); - }, - setTitles: function() { - $.each(key_assocs, function(keyval, sel) { - var menu = ($(sel).parents('#main_menu').length); - - $(sel).each(function() { - if(menu) { - var t = $(this).text().split(' [')[0]; - } else { - var t = this.title.split(' [')[0]; - } - var key_str = ''; - // Shift+Up - $.each(keyval.split('/'), function(i, key) { - var mod_bits = key.split('+'), mod = ''; - if(mod_bits.length > 1) { - mod = mod_bits[0] + '+'; - key = mod_bits[1]; - } - key_str += (i?'/':'') + mod + (uiStrings['key_'+key] || key); - }); - if(menu) { - this.lastChild.textContent = t +' ['+key_str+']'; - } else { - this.title = t +' ['+key_str+']'; - } - }); - }); - }, - getButtonData: function(sel) { - var b; - $.each(tool_buttons, function(i, btn) { - if(btn.sel === sel) b = btn; - }); - return b; - } - }; - }(); - - Actions.setAll(); - - // Select given tool - Editor.ready(function() { - var tool, - itool = curConfig.initTool, - container = $("#tools_left, #svg_editor .tools_flyout"), - pre_tool = container.find("#tool_" + itool), - reg_tool = container.find("#" + itool); - if(pre_tool.length) { - tool = pre_tool; - } else if(reg_tool.length){ - tool = reg_tool; - } else { - tool = $("#tool_select"); - } - tool.click().mouseup(); - - if(curConfig.wireframe) { - $('#tool_wireframe').click(); - } - - if(curConfig.showlayers) { - toggleSidePanel(); - } - - $('#rulers').toggle(!!curConfig.showRulers); - - if (curConfig.showRulers) { - $('#show_rulers')[0].checked = true; - } - - if(curConfig.gridSnapping) { - $('#grid_snapping_on')[0].checked = true; - } - - if(curConfig.baseUnit) { - $('#base_unit').val(curConfig.baseUnit); - } - - if(curConfig.snappingStep) { - $('#grid_snapping_step').val(curConfig.snappingStep); - } - }); - - $('#rect_rx').SpinButton({ min: 0, max: 1000, step: 1, callback: changeRectRadius }); - $('#stroke_width').SpinButton({ min: 0, max: 99, step: 1, smallStep: 0.1, callback: changeStrokeWidth }); - $('#angle').SpinButton({ min: -180, max: 180, step: 5, callback: changeRotationAngle }); - $('#font_size').SpinButton({ step: 1, min: 0.001, stepfunc: stepFontSize, callback: changeFontSize }); - $('#group_opacity').SpinButton({ step: 5, min: 0, max: 100, callback: changeOpacity }); - $('#blur').SpinButton({ step: .1, min: 0, max: 10, callback: changeBlur }); - $('#zoom').SpinButton({ min: 0.001, max: 10000, step: 50, stepfunc: stepZoom, callback: changeZoom }) - // Set default zoom - .val(svgCanvas.getZoom() * 100); - - $("#workarea").contextMenu({ - menu: 'cmenu_canvas', - inSpeed: 0 - }, - function(action, el, pos) { - switch ( action ) { - case 'delete': - deleteSelected(); - break; - case 'cut': - cutSelected(); - break; - case 'copy': - copySelected(); - break; - case 'paste': - svgCanvas.pasteElements(); - break; - case 'paste_in_place': - svgCanvas.pasteElements('in_place'); - break; - case 'group': - svgCanvas.groupSelectedElements(); - break; - case 'ungroup': - svgCanvas.ungroupSelectedElement(); - break; - case 'move_front': - moveToTopSelected(); - break; - case 'move_up': - moveUpDownSelected('Up'); - break; - case 'move_down': - moveUpDownSelected('Down'); - break; - case 'move_back': - moveToBottomSelected(); - break; - default: - if(svgedit.contextmenu && svgedit.contextmenu.hasCustomHandler(action)){ - svgedit.contextmenu.getCustomHandler(action).call(); - } - break; - } - - if(svgCanvas.clipBoard.length) { - canv_menu.enableContextMenuItems('#paste,#paste_in_place'); - } - }); - - var lmenu_func = function(action, el, pos) { - switch ( action ) { - case 'dupe': - cloneLayer(); - break; - case 'delete': - deleteLayer(); - break; - case 'merge_down': - mergeLayer(); - break; - case 'merge_all': - svgCanvas.mergeAllLayers(); - updateContextPanel(); - populateLayers(); - break; - } - } - - $("#layerlist").contextMenu({ - menu: 'cmenu_layers', - inSpeed: 0 - }, - lmenu_func - ); - - $("#layer_moreopts").contextMenu({ - menu: 'cmenu_layers', - inSpeed: 0, - allowLeft: true - }, - lmenu_func - ); - - $('.contextMenu li').mousedown(function(ev) { - ev.preventDefault(); - }) - - $('#cmenu_canvas li').disableContextMenu(); - canv_menu.enableContextMenuItems('#delete,#cut,#copy'); - - window.onbeforeunload = function() { - - if ('localStorage' in window) { - var name = 'svgedit-' + Editor.curConfig.canvasName; - window.localStorage.setItem(name, svgCanvas.getSvgString()); - Editor.show_save_warning = false; - } - - // Suppress warning if page is empty - if(undoMgr.getUndoStackSize() === 0) { - Editor.show_save_warning = false; - } - - // show_save_warning is set to "false" when the page is saved. - if(!curConfig.no_save_warning && Editor.show_save_warning) { - // Browser already asks question about closing the page - return uiStrings.notification.unsavedChanges; - } - }; - - Editor.openPrep = function(func) { - $('#main_menu').hide(); - if(undoMgr.getUndoStackSize() === 0) { - func(true); - } else { - $.confirm(uiStrings.notification.QwantToOpen, func); - } - } - - // use HTML5 File API: http://www.w3.org/TR/FileAPI/ - // if browser has HTML5 File API support, then we will show the open menu item - // and provide a file input to click. When that change event fires, it will - // get the text contents of the file and send it to the canvas - if (window.FileReader) { - var import_image = function(e) { - e.stopPropagation(); - e.preventDefault(); - $("#workarea").removeAttr("style"); - $('#main_menu').hide(); - var file = null; - if (e.type == "drop") file = e.dataTransfer.files[0] - else file = this.files[0]; - if (file) { - if(file.type.indexOf("image") != -1) { - //detected an image - //svg handling - if(file.type.indexOf("svg") != -1) { - var reader = new FileReader(); - reader.onloadend = function(e) { - svgCanvas.importSvgString(e.target.result, true); - svgCanvas.ungroupSelectedElement() - svgCanvas.ungroupSelectedElement() - svgCanvas.groupSelectedElements() - svgCanvas.alignSelectedElements("m", "page") - svgCanvas.alignSelectedElements("c", "page") - }; - reader.readAsText(file); - } - - //bitmap handling - else { - var reader = new FileReader(); - reader.onloadend = function(e) { - // let's insert the new image until we know its dimensions - insertNewImage = function(img_width, img_height){ - var newImage = svgCanvas.addSvgElementFromJson({ - "element": "image", - "attr": { - "x": 0, - "y": 0, - "width": img_width, - "height": img_height, - "id": svgCanvas.getNextId(), - "style": "pointer-events:inherit" - } - }); - svgCanvas.setHref(newImage, e.target.result); - svgCanvas.selectOnly([newImage]) - svgCanvas.alignSelectedElements("m", "page") - svgCanvas.alignSelectedElements("c", "page") - updateContextPanel(); - } - // create dummy img so we know the default dimensions - var img_width = 100; - var img_height = 100; - var img = new Image(); - img.src = e.target.result; - img.style.opacity = 0; - img.onload = function() { - img_width = img.offsetWidth - img_height = img.offsetHeight - insertNewImage(img_width, img_height); - } - }; - reader.readAsDataURL(file) - } - } - } - } - - function onDragEnter(e) { - e.stopPropagation(); - e.preventDefault(); - // and indicator should be displayed here, such as "drop files here" - } - - function onDragOver(e) { - e.stopPropagation(); - e.preventDefault(); - } - - function onDragLeave(e) { - e.stopPropagation(); - e.preventDefault(); - // hypothetical indicator should be removed here - } - - workarea[0].addEventListener('dragenter', onDragEnter, false); - workarea[0].addEventListener('dragover', onDragOver, false); - workarea[0].addEventListener('dragleave', onDragLeave, false); - workarea[0].addEventListener('drop', import_image, false); - - var open = $('<input type="file">').change(function() { - var f = this; - Editor.openPrep(function(ok) { - if(!ok) return; - svgCanvas.clear(); - if(f.files.length==1) { - var reader = new FileReader(); - reader.onloadend = function(e) { - loadSvgString(e.target.result); - updateCanvas(); - }; - reader.readAsText(f.files[0]); - } - }); - }); - $("#tool_open").show().prepend(open); - - var img_import = $('<input type="file">').change(import_image); - $("#tool_import").show().prepend(img_import); - } - - var updateCanvas = Editor.updateCanvas = function(center, new_ctr) { - - var w = workarea.width(), h = workarea.height(); - var w_orig = w, h_orig = h; - var zoom = svgCanvas.getZoom(); - var w_area = workarea; - var cnvs = $("#svgcanvas"); - - var old_ctr = { - x: w_area[0].scrollLeft + w_orig/2, - y: w_area[0].scrollTop + h_orig/2 - }; - - var multi = curConfig.canvas_expansion; - w = Math.max(w_orig, svgCanvas.contentW * zoom * multi); - h = Math.max(h_orig, svgCanvas.contentH * zoom * multi); - - if(w == w_orig && h == h_orig) { - workarea.css('overflow','hidden'); - } else { - workarea.css('overflow','scroll'); - } - - var old_can_y = cnvs.height()/2; - var old_can_x = cnvs.width()/2; - cnvs.width(w).height(h); - var new_can_y = h/2; - var new_can_x = w/2; - var offset = svgCanvas.updateCanvas(w, h); - - var ratio = new_can_x / old_can_x; - - var scroll_x = w/2 - w_orig/2; - var scroll_y = h/2 - h_orig/2; - - if(!new_ctr) { - - var old_dist_x = old_ctr.x - old_can_x; - var new_x = new_can_x + old_dist_x * ratio; - - var old_dist_y = old_ctr.y - old_can_y; - var new_y = new_can_y + old_dist_y * ratio; - - new_ctr = { - x: new_x, - y: new_y - }; - - } else { - new_ctr.x += offset.x, - new_ctr.y += offset.y; - } - - if(center) { - // Go to top-left for larger documents - if(svgCanvas.contentW > w_area.width()) { - // Top-left - workarea[0].scrollLeft = offset.x - 10; - workarea[0].scrollTop = offset.y - 10; - } else { - // Center - w_area[0].scrollLeft = scroll_x; - w_area[0].scrollTop = scroll_y; - } - } else { - w_area[0].scrollLeft = new_ctr.x - w_orig/2; - w_area[0].scrollTop = new_ctr.y - h_orig/2; - } - if(curConfig.showRulers) { - updateRulers(cnvs, zoom); - workarea.scroll(); - } - } - - // Make [1,2,5] array - var r_intervals = []; - for(var i = .1; i < 1E5; i *= 10) { - r_intervals.push(1 * i); - r_intervals.push(2 * i); - r_intervals.push(5 * i); - } - - function updateRulers(scanvas, zoom) { - if(!zoom) zoom = svgCanvas.getZoom(); - if(!scanvas) scanvas = $("#svgcanvas"); - - var limit = 30000; - - var c_elem = svgCanvas.getContentElem(); - - var units = svgedit.units.getTypeMap(); - var unit = units[curConfig.baseUnit]; // 1 = 1px - - for(var d = 0; d < 2; d++) { - var is_x = (d === 0); - var dim = is_x ? 'x' : 'y'; - var lentype = is_x?'width':'height'; - var content_d = c_elem.getAttribute(dim)-0; - - var $hcanv_orig = $('#ruler_' + dim + ' canvas:first'); - - // Bit of a hack to fully clear the canvas in Safari & IE9 - $hcanv = $hcanv_orig.clone(); - $hcanv_orig.replaceWith($hcanv); - - var hcanv = $hcanv[0]; - - // Set the canvas size to the width of the container - var ruler_len = scanvas[lentype](); - var total_len = ruler_len; - hcanv.parentNode.style[lentype] = total_len + 'px'; - - - var canv_count = 1; - var ctx_num = 0; - var ctx_arr; - var ctx = hcanv.getContext("2d"); - - ctx.fillStyle = "rgb(200,0,0)"; - ctx.fillRect(0,0,hcanv.width,hcanv.height); - - // Remove any existing canvasses - $hcanv.siblings().remove(); - - // Create multiple canvases when necessary (due to browser limits) - if(ruler_len >= limit) { - var num = parseInt(ruler_len / limit) + 1; - ctx_arr = Array(num); - ctx_arr[0] = ctx; - for(var i = 1; i < num; i++) { - hcanv[lentype] = limit; - var copy = hcanv.cloneNode(true); - hcanv.parentNode.appendChild(copy); - ctx_arr[i] = copy.getContext('2d'); - } - - copy[lentype] = ruler_len % limit; - - // set copy width to last - ruler_len = limit; - } - - hcanv[lentype] = ruler_len; - - var u_multi = unit * zoom; - - // Calculate the main number interval - var raw_m = 50 / u_multi; - var multi = 1; - for(var i = 0; i < r_intervals.length; i++) { - var num = r_intervals[i]; - multi = num; - if(raw_m <= num) { - break; - } - } - - var big_int = multi * u_multi; - - ctx.font = "9px sans-serif"; - - var ruler_d = ((content_d / u_multi) % multi) * u_multi; - var label_pos = ruler_d - big_int; - for (; ruler_d < total_len; ruler_d += big_int) { - label_pos += big_int; - var real_d = ruler_d - content_d; - - var cur_d = Math.round(ruler_d) + .5; - if(is_x) { - ctx.moveTo(cur_d, 15); - ctx.lineTo(cur_d, 0); - } else { - ctx.moveTo(15, cur_d); - ctx.lineTo(0, cur_d); - } - - var num = (label_pos - content_d) / u_multi; - var label; - if(multi >= 1) { - label = Math.round(num); - } else { - var decs = (multi+'').split('.')[1].length; - label = num.toFixed(decs)-0; - } - - // Do anything special for negative numbers? -// var is_neg = label < 0; -// real_d2 = Math.abs(real_d2); - - // Change 1000s to Ks - if(label !== 0 && label !== 1000 && label % 1000 === 0) { - label = (label / 1000) + 'K'; - } - - if(is_x) { - ctx.fillText(label, ruler_d+2, 8); - } else { - var str = (label+'').split(''); - for(var i = 0; i < str.length; i++) { - ctx.fillText(str[i], 1, (ruler_d+9) + i*9); - } - } - - var part = big_int / 10; - for(var i = 1; i < 10; i++) { - var sub_d = Math.round(ruler_d + part * i) + .5; - if(ctx_arr && sub_d > ruler_len) { - ctx_num++; - ctx.stroke(); - if(ctx_num >= ctx_arr.length) { - i = 10; - ruler_d = total_len; - continue; - } - ctx = ctx_arr[ctx_num]; - ruler_d -= limit; - sub_d = Math.round(ruler_d + part * i) + .5; - } - - var line_num = (i % 2)?12:10; - if(is_x) { - ctx.moveTo(sub_d, 15); - ctx.lineTo(sub_d, line_num); - } else { - ctx.moveTo(15, sub_d); - ctx.lineTo(line_num ,sub_d); - } - } - } - - // console.log('ctx', ctx); - ctx.strokeStyle = "#000"; - ctx.stroke(); - } - } - -// $(function() { - updateCanvas(true); -// }); - - // var revnums = "svg-editor.js ($Rev: 2277 $) "; - // revnums += svgCanvas.getVersion(); - // $('#copyright')[0].setAttribute("title", revnums); - - // Callback handler for embedapi.js - try{ - var json_encode = function(obj){ - //simple partial JSON encoder implementation - if(window.JSON && JSON.stringify) return JSON.stringify(obj); - var enc = arguments.callee; //for purposes of recursion - if(typeof obj == "boolean" || typeof obj == "number"){ - return obj+'' //should work... - }else if(typeof obj == "string"){ - //a large portion of this is stolen from Douglas Crockford's json2.js - return '"'+ - obj.replace( - /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g - , function (a) { - return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) - +'"'; //note that this isn't quite as purtyful as the usualness - }else if(obj.length){ //simple hackish test for arrayish-ness - for(var i = 0; i < obj.length; i++){ - obj[i] = enc(obj[i]); //encode every sub-thingy on top - } - return "["+obj.join(",")+"]"; - }else{ - var pairs = []; //pairs will be stored here - for(var k in obj){ //loop through thingys - pairs.push(enc(k)+":"+enc(obj[k])); //key: value - } - return "{"+pairs.join(",")+"}" //wrap in the braces - } - } - window.addEventListener("message", function(e){ - var cbid = parseInt(e.data.substr(0, e.data.indexOf(";"))); - try{ - e.source.postMessage("SVGe"+cbid+";"+json_encode(eval(e.data)), "*"); - }catch(err){ - e.source.postMessage("SVGe"+cbid+";error:"+err.message, "*"); - } - }, false) - }catch(err){ - window.embed_error = err; - } - - - - // For Compatibility with older extensions - $(function() { - window.svgCanvas = svgCanvas; - svgCanvas.ready = svgEditor.ready; - }); - - - Editor.setLang = function(lang, allStrings) { - $.pref('lang', lang); - $('#lang_select').val(lang); - if(allStrings) { - - var notif = allStrings.notification; - - - - // $.extend will only replace the given strings - var oldLayerName = $('#layerlist tr.layersel td.layername').text(); - var rename_layer = (oldLayerName == uiStrings.common.layer + ' 1'); - - $.extend(uiStrings, allStrings); - svgCanvas.setUiStrings(allStrings); - Actions.setTitles(); - - if(rename_layer) { - svgCanvas.renameCurrentLayer(uiStrings.common.layer + ' 1'); - populateLayers(); - } - - svgCanvas.runExtensions("langChanged", lang); - - // Update flyout tooltips - setFlyoutTitles(); - - // Copy title for certain tool elements - var elems = { - '#stroke_color': '#tool_stroke .icon_label, #tool_stroke .color_block', - '#fill_color': '#tool_fill label, #tool_fill .color_block', - '#linejoin_miter': '#cur_linejoin', - '#linecap_butt': '#cur_linecap' - } - - $.each(elems, function(source, dest) { - $(dest).attr('title', $(source)[0].title); - }); - - // Copy alignment titles - $('#multiselected_panel div[id^=tool_align]').each(function() { - $('#tool_pos' + this.id.substr(10))[0].title = this.title; - }); - - } - }; - }; - - var callbacks = []; - - function loadSvgString(str, callback) { - var success = svgCanvas.setSvgString(str) !== false; - callback = callback || $.noop; - if(success) { - callback(true); - } else { - $.alert(uiStrings.notification.errorLoadingSVG, function() { - callback(false); - }); - } - } - - Editor.ready = function(cb) { - if(!is_ready) { - callbacks.push(cb); - } else { - cb(); - } - }; - - Editor.runCallbacks = function() { - $.each(callbacks, function() { - this(); - }); - is_ready = true; - }; - - Editor.loadFromString = function(str) { - Editor.ready(function() { - loadSvgString(str); - }); - }; - - Editor.disableUI = function(featList) { -// $(function() { -// $('#tool_wireframe, #tool_image, #main_button, #tool_source, #sidepanels').remove(); -// $('#tools_top').css('left', 5); -// }); - }; - - Editor.loadFromURL = function(url, opts) { - if(!opts) opts = {}; - - var cache = opts.cache; - var cb = opts.callback; - - Editor.ready(function() { - $.ajax({ - 'url': url, - 'dataType': 'text', - cache: !!cache, - success: function(str) { - loadSvgString(str, cb); - }, - error: function(xhr, stat, err) { - if(xhr.status != 404 && xhr.responseText) { - loadSvgString(xhr.responseText, cb); - } else { - $.alert(uiStrings.notification.URLloadFail + ": \n"+err+'', cb); - } - } - }); - }); - }; - - Editor.loadFromDataURI = function(str) { - Editor.ready(function() { - var pre = 'data:image/svg+xml;base64,'; - var src = str.substring(pre.length); - loadSvgString(svgedit.utilities.decode64(src)); - }); - }; - - Editor.addExtension = function() { - var args = arguments; - - // Note that we don't want this on Editor.ready since some extensions - // may want to run before then (like server_opensave). - $(function() { - if(svgCanvas) svgCanvas.addExtension.apply(this, args); - }); - }; - - return Editor; - }(jQuery); - - // Run init once DOM is loaded - $(svgEditor.init); - -})(); - -// ?iconsize=s&bkgd_color=555 - -// svgEditor.setConfig({ -// // imgPath: 'foo', -// dimensions: [800, 600], -// canvas_expansion: 5, -// initStroke: { -// color: '0000FF', -// width: 3.5, -// opacity: .5 -// }, -// initFill: { -// color: '550000', -// opacity: .75 -// }, -// extensions: ['ext-helloworld.js'] -// }) diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svg-editor.manifest b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svg-editor.manifest deleted file mode 100644 index b1563746..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svg-editor.manifest +++ /dev/null @@ -1,121 +0,0 @@ -CACHE MANIFEST -svg-editor.html -images/logo.png -jgraduate/css/jPicker-1.0.9.css -jgraduate/css/jGraduate-0.2.0.css -svg-editor.css -spinbtn/JQuerySpinBtn.css -jquery.js -js-hotkeys/jquery.hotkeys.min.js -jquery-ui/jquery-ui-1.7.2.custom.min.js -jgraduate/jpicker-1.0.9.min.js -jgraduate/jquery.jgraduate.js -spinbtn/JQuerySpinBtn.js -svgcanvas.js -svg-editor.js -images/align-bottom.png -images/align-center.png -images/align-left.png -images/align-middle.png -images/align-right.png -images/align-top.png -images/bold.png -images/cancel.png -images/circle.png -images/clear.png -images/clone.png -images/copy.png -images/cut.png -images/delete.png -images/document-properties.png -images/dropdown.gif -images/ellipse.png -images/eye.png -images/flyouth.png -images/flyup.gif -images/freehand-circle.png -images/freehand-square.png -images/go-down.png -images/go-up.png -images/image.png -images/italic.png -images/line.png -images/logo.png -images/logo.svg -images/move_bottom.png -images/move_top.png -images/none.png -images/open.png -images/paste.png -images/path.png -images/polygon.png -images/rect.png -images/redo.png -images/save.png -images/select.png -images/sep.png -images/shape_group.png -images/shape_ungroup.png -images/source.png -images/square.png -images/text.png -images/undo.png -images/view-refresh.png -images/wave.png -images/zoom.png -locale/locale.js -locale/lang.af.js -locale/lang.ar.js -locale/lang.az.js -locale/lang.be.js -locale/lang.bg.js -locale/lang.ca.js -locale/lang.cs.js -locale/lang.cy.js -locale/lang.da.js -locale/lang.de.js -locale/lang.el.js -locale/lang.en.js -locale/lang.es.js -locale/lang.et.js -locale/lang.fa.js -locale/lang.fi.js -locale/lang.fr.js -locale/lang.ga.js -locale/lang.gl.js -locale/lang.hi.js -locale/lang.hr.js -locale/lang.hu.js -locale/lang.hy.js -locale/lang.id.js -locale/lang.is.js -locale/lang.it.js -locale/lang.iw.js -locale/lang.ja.js -locale/lang.ko.js -locale/lang.lt.js -locale/lang.lv.js -locale/lang.mk.js -locale/lang.ms.js -locale/lang.mt.js -locale/lang.nl.js -locale/lang.no.js -locale/lang.pl.js -locale/lang.pt-PT.js -locale/lang.ro.js -locale/lang.ru.js -locale/lang.sk.js -locale/lang.sl.js -locale/lang.sq.js -locale/lang.sr.js -locale/lang.sv.js -locale/lang.sw.js -locale/lang.th.js -locale/lang.tl.js -locale/lang.tr.js -locale/lang.uk.js -locale/lang.vi.js -locale/lang.yi.js -locale/lang.zh-CN.js -locale/lang.zh-TW.js -locale/lang.zh.js diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgcanvas.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgcanvas.js deleted file mode 100644 index 51a1cb9a..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgcanvas.js +++ /dev/null @@ -1,8820 +0,0 @@ -/* - * svgcanvas.js - * - * Licensed under the MIT License - * - * Copyright(c) 2010 Alexis Deveria - * Copyright(c) 2010 Pavol Rusnak - * Copyright(c) 2010 Jeff Schiller - * Copyright(c) 2016 Glutanimate - * - */ - -// Dependencies: -// 1) jQuery -// 2) browser.js -// 3) svgtransformlist.js -// 4) math.js -// 5) units.js -// 6) svgutils.js -// 7) sanitize.js -// 8) history.js -// 9) select.js -// 10) draw.js -// 11) path.js - -// This version of svg-edit has been adapted to work with Image Occlusion Enhanced - -// Modifications not specifically documented in commits: -// - replace mousewheel zoom code with code in SVG-edit 2.7 -// - this fixes a longstanding issue where mousewheel zoom wouldn't -// work properly on the canvas -// - change hotkey for mousewheel zoom to CTRL + Mousewheel -// - merge magnebra/add_text_config_options -// -// Please consult the commit log for all other changes to this file - - -if(!window.console) { - window.console = {}; - window.console.log = function(str) {}; - window.console.dir = function(str) {}; -} - -if(window.opera) { - window.console.log = function(str) { opera.postError(str); }; - window.console.dir = function(str) {}; -} - -(function() { - - // This fixes $(...).attr() to work as expected with SVG elements. - // Does not currently use *AttributeNS() since we rarely need that. - - // See http://api.jquery.com/attr/ for basic documentation of .attr() - - // Additional functionality: - // - When getting attributes, a string that's a number is return as type number. - // - If an array is supplied as first parameter, multiple values are returned - // as an object with values for each given attributes - - var proxied = jQuery.fn.attr, svgns = "http://www.w3.org/2000/svg"; - jQuery.fn.attr = function(key, value) { - var len = this.length; - if(!len) return proxied.apply(this, arguments); - for(var i=0; i<len; i++) { - var elem = this[i]; - // set/get SVG attribute - if(elem.namespaceURI === svgns) { - // Setting attribute - if(value !== undefined) { - elem.setAttribute(key, value); - } else if($.isArray(key)) { - // Getting attributes from array - var j = key.length, obj = {}; - - while(j--) { - var aname = key[j]; - var attr = elem.getAttribute(aname); - // This returns a number when appropriate - if(attr || attr === "0") { - attr = isNaN(attr)?attr:attr-0; - } - obj[aname] = attr; - } - return obj; - - } else if(typeof key === "object") { - // Setting attributes form object - for(var v in key) { - elem.setAttribute(v, key[v]); - } - // Getting attribute - } else { - var attr = elem.getAttribute(key); - if(attr || attr === "0") { - attr = isNaN(attr)?attr:attr-0; - } - - return attr; - } - } else { - return proxied.apply(this, arguments); - } - } - return this; - }; - -}()); - -// Class: SvgCanvas -// The main SvgCanvas class that manages all SVG-related functions -// -// Parameters: -// container - The container HTML element that should hold the SVG root element -// config - An object that contains configuration data -$.SvgCanvas = function(container, config) -{ -// Namespace constants -var svgns = "http://www.w3.org/2000/svg", - xlinkns = "http://www.w3.org/1999/xlink", - xmlns = "http://www.w3.org/XML/1998/namespace", - xmlnsns = "http://www.w3.org/2000/xmlns/", // see http://www.w3.org/TR/REC-xml-names/#xmlReserved - se_ns = "http://svg-edit.googlecode.com", - htmlns = "http://www.w3.org/1999/xhtml", - mathns = "http://www.w3.org/1998/Math/MathML"; - -// Default configuration options -var curConfig = { - show_outside_canvas: true, - selectNew: true, - dimensions: [640, 480] -}; - -// Update config with new one if given -if(config) { - $.extend(curConfig, config); -} - -// Array with width/height of canvas -var dimensions = curConfig.dimensions; - -var canvas = this; - -// "document" element associated with the container (same as window.document using default svg-editor.js) -// NOTE: This is not actually a SVG document, but a HTML document. -var svgdoc = container.ownerDocument; - -// This is a container for the document being edited, not the document itself. -var svgroot = svgdoc.importNode(svgedit.utilities.text2xml( - '<svg id="svgroot" xmlns="' + svgns + '" xlinkns="' + xlinkns + '" ' + - 'width="' + dimensions[0] + '" height="' + dimensions[1] + '" x="' + dimensions[0] + '" y="' + dimensions[1] + '" overflow="visible">' + - '<defs>' + - '<filter id="canvashadow" filterUnits="objectBoundingBox">' + - '<feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/>'+ - '<feOffset in="blur" dx="5" dy="5" result="offsetBlur"/>'+ - '<feMerge>'+ - '<feMergeNode in="offsetBlur"/>'+ - '<feMergeNode in="SourceGraphic"/>'+ - '</feMerge>'+ - '</filter>'+ - '</defs>'+ - '</svg>').documentElement, true); -container.appendChild(svgroot); - -// The actual element that represents the final output SVG element -var svgcontent = svgdoc.createElementNS(svgns, "svg"); - -// This function resets the svgcontent element while keeping it in the DOM. -var clearSvgContentElement = canvas.clearSvgContentElement = function() { - while (svgcontent.firstChild) { svgcontent.removeChild(svgcontent.firstChild); } - - // TODO: Clear out all other attributes first? - $(svgcontent).attr({ - id: 'svgcontent', - width: dimensions[0], - height: dimensions[1], - x: dimensions[0], - y: dimensions[1], - overflow: curConfig.show_outside_canvas ? 'visible' : 'hidden', - xmlns: svgns, - "xmlns:se": se_ns, - "xmlns:xlink": xlinkns - }).appendTo(svgroot); - - // TODO: make this string optional and set by the client - var comment = svgdoc.createComment(" Created with Image Occlusion Enhanced "); - svgcontent.appendChild(comment); -}; -clearSvgContentElement(); - -// Prefix string for element IDs -var idprefix = "svg_"; - -// Function: setIdPrefix -// Changes the ID prefix to the given value -// -// Parameters: -// p - String with the new prefix -canvas.setIdPrefix = function(p) { - idprefix = p; -}; - -// Current svgedit.draw.Drawing object -// @type {svgedit.draw.Drawing} -canvas.current_drawing_ = new svgedit.draw.Drawing(svgcontent, idprefix); - -// Function: getCurrentDrawing -// Returns the current Drawing. -// @return {svgedit.draw.Drawing} -var getCurrentDrawing = canvas.getCurrentDrawing = function() { - return canvas.current_drawing_; -}; - -// Float displaying the current zoom level (1 = 100%, .5 = 50%, etc) -var current_zoom = 1; - -// pointer to current group (for in-group editing) -var current_group = null; - -// Object containing data for the currently selected styles -var all_properties = { - shape: { - fill: (curConfig.initFill.color == 'none' ? '' : '#') + curConfig.initFill.color, - fill_paint: null, - fill_opacity: curConfig.initFill.opacity, - stroke: "#" + curConfig.initStroke.color, - stroke_paint: null, - stroke_opacity: curConfig.initStroke.opacity, - stroke_width: curConfig.initStroke.width, - stroke_dasharray: 'none', - stroke_linejoin: 'miter', - stroke_linecap: 'butt', - opacity: curConfig.initOpacity - } -}; - -all_properties.text = $.extend(true, {}, all_properties.shape); -$.extend(all_properties.text, { - fill: "#000000", - stroke_width: curConfig.text.stroke_width, - font_size: curConfig.text.font_size, - font_family: curConfig.text.font_family -}); - -// Current shape style properties -var cur_shape = all_properties.shape; - -// Array with all the currently selected elements -// default size of 1 until it needs to grow bigger -var selectedElements = new Array(1); - -// Function: addSvgElementFromJson -// Create a new SVG element based on the given object keys/values and add it to the current layer -// The element will be ran through cleanupElement before being returned -// -// Parameters: -// data - Object with the following keys/values: -// * element - tag name of the SVG element to create -// * attr - Object with attributes key-values to assign to the new element -// * curStyles - Boolean indicating that current style attributes should be applied first -// -// Returns: The new element -var addSvgElementFromJson = this.addSvgElementFromJson = function(data) { - var shape = svgedit.utilities.getElem(data.attr.id); - // if shape is a path but we need to create a rect/ellipse, then remove the path - var current_layer = getCurrentDrawing().getCurrentLayer(); - if (shape && data.element != shape.tagName) { - current_layer.removeChild(shape); - shape = null; - } - if (!shape) { - shape = svgdoc.createElementNS(svgns, data.element); - if (current_layer) { - (current_group || current_layer).appendChild(shape); - } - } - if(data.curStyles) { - svgedit.utilities.assignAttributes(shape, { - "fill": cur_shape.fill, - "stroke": cur_shape.stroke, - "stroke-width": cur_shape.stroke_width, - "stroke-dasharray": cur_shape.stroke_dasharray, - "stroke-linejoin": cur_shape.stroke_linejoin, - "stroke-linecap": cur_shape.stroke_linecap, - "stroke-opacity": cur_shape.stroke_opacity, - "fill-opacity": cur_shape.fill_opacity, - "opacity": cur_shape.opacity, - "style": "pointer-events:inherit" - }, 100); - } - svgedit.utilities.assignAttributes(shape, data.attr, 100); - svgedit.utilities.cleanupElement(shape); - return shape; -}; - - -// import svgtransformlist.js -var getTransformList = canvas.getTransformList = svgedit.transformlist.getTransformList; - -// import from math.js. -var transformPoint = svgedit.math.transformPoint; -var matrixMultiply = canvas.matrixMultiply = svgedit.math.matrixMultiply; -var hasMatrixTransform = canvas.hasMatrixTransform = svgedit.math.hasMatrixTransform; -var transformListToTransform = canvas.transformListToTransform = svgedit.math.transformListToTransform; -var snapToAngle = svgedit.math.snapToAngle; -var getMatrix = svgedit.math.getMatrix; - -// initialize from units.js -// send in an object implementing the ElementContainer interface (see units.js) -svgedit.units.init({ - getBaseUnit: function() { return curConfig.baseUnit; }, - getElement: svgedit.utilities.getElem, - getHeight: function() { return svgcontent.getAttribute("height")/current_zoom; }, - getWidth: function() { return svgcontent.getAttribute("width")/current_zoom; }, - getRoundDigits: function() { return save_options.round_digits; } -}); -// import from units.js -var convertToNum = canvas.convertToNum = svgedit.units.convertToNum; - -// import from svgutils.js -svgedit.utilities.init({ - getDOMDocument: function() { return svgdoc; }, - getDOMContainer: function() { return container; }, - getSVGRoot: function() { return svgroot; }, - // TODO: replace this mostly with a way to get the current drawing. - getSelectedElements: function() { return selectedElements; }, - getSVGContent: function() { return svgcontent; } -}); -var getUrlFromAttr = canvas.getUrlFromAttr = svgedit.utilities.getUrlFromAttr; -var getHref = canvas.getHref = svgedit.utilities.getHref; -var setHref = canvas.setHref = svgedit.utilities.setHref; -var getPathBBox = svgedit.utilities.getPathBBox; -var getBBox = canvas.getBBox = svgedit.utilities.getBBox; -var getRotationAngle = canvas.getRotationAngle = svgedit.utilities.getRotationAngle; -var getElem = canvas.getElem = svgedit.utilities.getElem; -var assignAttributes = canvas.assignAttributes = svgedit.utilities.assignAttributes; -var cleanupElement = this.cleanupElement = svgedit.utilities.cleanupElement; - -// import from sanitize.js -var nsMap = svgedit.sanitize.getNSMap(); -var sanitizeSvg = canvas.sanitizeSvg = svgedit.sanitize.sanitizeSvg; - -// import from history.js -var MoveElementCommand = svgedit.history.MoveElementCommand; -var InsertElementCommand = svgedit.history.InsertElementCommand; -var RemoveElementCommand = svgedit.history.RemoveElementCommand; -var ChangeElementCommand = svgedit.history.ChangeElementCommand; -var BatchCommand = svgedit.history.BatchCommand; -// Implement the svgedit.history.HistoryEventHandler interface. -canvas.undoMgr = new svgedit.history.UndoManager({ - handleHistoryEvent: function(eventType, cmd) { - var EventTypes = svgedit.history.HistoryEventTypes; - // TODO: handle setBlurOffsets. - if (eventType == EventTypes.BEFORE_UNAPPLY || eventType == EventTypes.BEFORE_APPLY) { - canvas.clearSelection(); - } else if (eventType == EventTypes.AFTER_APPLY || eventType == EventTypes.AFTER_UNAPPLY) { - var elems = cmd.elements(); - canvas.pathActions.clear(); - call("changed", elems); - - var cmdType = cmd.type(); - var isApply = (eventType == EventTypes.AFTER_APPLY); - if (cmdType == MoveElementCommand.type()) { - var parent = isApply ? cmd.newParent : cmd.oldParent; - if (parent == svgcontent) { - canvas.identifyLayers(); - } - } else if (cmdType == InsertElementCommand.type() || - cmdType == RemoveElementCommand.type()) { - if (cmd.parent == svgcontent) { - canvas.identifyLayers(); - } - if (cmdType == InsertElementCommand.type()) { - if (isApply) restoreRefElems(cmd.elem); - } else { - if (!isApply) restoreRefElems(cmd.elem); - } - - if(cmd.elem.tagName === 'use') { - setUseData(cmd.elem); - } - } else if (cmdType == ChangeElementCommand.type()) { - // if we are changing layer names, re-identify all layers - if (cmd.elem.tagName == "title" && cmd.elem.parentNode.parentNode == svgcontent) { - canvas.identifyLayers(); - } - var values = isApply ? cmd.newValues : cmd.oldValues; - // If stdDeviation was changed, update the blur. - if (values["stdDeviation"]) { - canvas.setBlurOffsets(cmd.elem.parentNode, values["stdDeviation"]); - } - // This is resolved in later versions of webkit, perhaps we should - // have a featured detection for correct 'use' behavior? - // —————————— - // Remove & Re-add hack for Webkit (issue 775) - //if(cmd.elem.tagName === 'use' && svgedit.browser.isWebkit()) { - // var elem = cmd.elem; - // if(!elem.getAttribute('x') && !elem.getAttribute('y')) { - // var parent = elem.parentNode; - // var sib = elem.nextSibling; - // parent.removeChild(elem); - // parent.insertBefore(elem, sib); - // } - //} - } - } - } -}); -var addCommandToHistory = function(cmd) { - canvas.undoMgr.addCommandToHistory(cmd); -}; - -// import from select.js -svgedit.select.init(curConfig, { - createSVGElement: function(jsonMap) { return canvas.addSvgElementFromJson(jsonMap); }, - svgRoot: function() { return svgroot; }, - svgContent: function() { return svgcontent; }, - currentZoom: function() { return current_zoom; }, - // TODO(codedread): Remove when getStrokedBBox() has been put into svgutils.js. - getStrokedBBox: function(elems) { return canvas.getStrokedBBox([elems]); } -}); -// this object manages selectors for us -var selectorManager = this.selectorManager = svgedit.select.getSelectorManager(); - -// Import from path.js -svgedit.path.init({ - getCurrentZoom: function() { return current_zoom; }, - getSVGRoot: function() { return svgroot; } -}); - -// Function: snapToGrid -// round value to for snapping -// NOTE: This function did not move to svgutils.js since it depends on curConfig. -svgedit.utilities.snapToGrid = function(value){ - var stepSize = curConfig.snappingStep; - var unit = curConfig.baseUnit; - if(unit !== "px") { - stepSize *= svgedit.units.getTypeMap()[unit]; - } - value = Math.round(value/stepSize)*stepSize; - return value; -}; -var snapToGrid = svgedit.utilities.snapToGrid; - -// Interface strings, usually for title elements -var uiStrings = { - "exportNoBlur": "Blurred elements will appear as un-blurred", - "exportNoforeignObject": "foreignObject elements will not appear", - "exportNoDashArray": "Strokes will appear filled", - "exportNoText": "Text may not appear as expected" -}; - -var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'; -var ref_attrs = ["clip-path", "fill", "filter", "marker-end", "marker-mid", "marker-start", "mask", "stroke"]; - -var elData = $.data; - -// Animation element to change the opacity of any newly created element -var opac_ani = document.createElementNS(svgns, 'animate'); -$(opac_ani).attr({ - attributeName: 'opacity', - begin: 'indefinite', - dur: 1, - fill: 'freeze' -}).appendTo(svgroot); - -var restoreRefElems = function(elem) { - // Look for missing reference elements, restore any found - var attrs = $(elem).attr(ref_attrs); - for(var o in attrs) { - var val = attrs[o]; - if (val && val.indexOf('url(') === 0) { - var id = getUrlFromAttr(val).substr(1); - var ref = getElem(id); - if(!ref) { - findDefs().appendChild(removedElements[id]); - delete removedElements[id]; - } - } - } - - var childs = elem.getElementsByTagName('*'); - - if(childs.length) { - for(var i = 0, l = childs.length; i < l; i++) { - restoreRefElems(childs[i]); - } - } -}; - -(function() { - // TODO For Issue 208: this is a start on a thumbnail - // var svgthumb = svgdoc.createElementNS(svgns, "use"); - // svgthumb.setAttribute('width', '100'); - // svgthumb.setAttribute('height', '100'); - // svgedit.utilities.setHref(svgthumb, '#svgcontent'); - // svgroot.appendChild(svgthumb); - -})(); - -// Object to contain image data for raster images that were found encodable -var encodableImages = {}, - - // String with image URL of last loadable image - last_good_img_url = curConfig.imgPath + 'logo.png', - - // Array with current disabled elements (for in-group editing) - disabled_elems = [], - - // Object with save options - save_options = {round_digits: 5}, - - // Boolean indicating whether or not a draw action has been started - started = false, - - // String with an element's initial transform attribute value - start_transform = null, - - // String indicating the current editor mode - current_mode = "select", - - // String with the current direction in which an element is being resized - current_resize_mode = "none", - - // Object with IDs for imported files, to see if one was already added - import_ids = {}; - -// Current text style properties -var cur_text = all_properties.text, - - // Current general properties - cur_properties = cur_shape, - - // Array with selected elements' Bounding box object -// selectedBBoxes = new Array(1), - - // The DOM element that was just selected - justSelected = null, - - // DOM element for selection rectangle drawn by the user - rubberBox = null, - - // Array of current BBoxes (still needed?) - curBBoxes = [], - - // Object to contain all included extensions - extensions = {}, - - // Canvas point for the most recent right click - lastClickPoint = null, - - // Map of deleted reference elements - removedElements = {} - -// Clipboard for cut, copy&pasted elements -canvas.clipBoard = []; - -// Should this return an array by default, so extension results aren't overwritten? -var runExtensions = this.runExtensions = function(action, vars, returnArray) { - var result = false; - if(returnArray) result = []; - $.each(extensions, function(name, opts) { - if(action in opts) { - if(returnArray) { - result.push(opts[action](vars)) - } else { - result = opts[action](vars); - } - } - }); - return result; -} - -// Function: addExtension -// Add an extension to the editor -// -// Parameters: -// name - String with the ID of the extension -// ext_func - Function supplied by the extension with its data -this.addExtension = function(name, ext_func) { - if(!(name in extensions)) { - // Provide private vars/funcs here. Is there a better way to do this? - - if($.isFunction(ext_func)) { - var ext = ext_func($.extend(canvas.getPrivateMethods(), { - svgroot: svgroot, - svgcontent: svgcontent, - nonce: getCurrentDrawing().getNonce(), - selectorManager: selectorManager - })); - } else { - var ext = ext_func; - } - extensions[name] = ext; - call("extension_added", ext); - } else { - console.log('Cannot add extension "' + name + '", an extension by that name already exists"'); - } -}; - -// This method rounds the incoming value to the nearest value based on the current_zoom -var round = this.round = function(val) { - return parseInt(val*current_zoom)/current_zoom; -}; - -// This method sends back an array or a NodeList full of elements that -// intersect the multi-select rubber-band-box on the current_layer only. -// -// Since the only browser that supports the SVG DOM getIntersectionList is Opera, -// we need to provide an implementation here. We brute-force it for now. -// -// Reference: -// Firefox does not implement getIntersectionList(), see https://bugzilla.mozilla.org/show_bug.cgi?id=501421 -// Webkit does not implement getIntersectionList(), see https://bugs.webkit.org/show_bug.cgi?id=11274 -var getIntersectionList = this.getIntersectionList = function(rect) { - if (rubberBox == null) { return null; } - - var parent = current_group || getCurrentDrawing().getCurrentLayer(); - - if(!curBBoxes.length) { - // Cache all bboxes - curBBoxes = getVisibleElementsAndBBoxes(parent); - } - - var resultList = null; - try { - resultList = parent.getIntersectionList(rect, null); - } catch(e) { } - - if (resultList == null || typeof(resultList.item) != "function") { - resultList = []; - - if(!rect) { - var rubberBBox = rubberBox.getBBox(); - var bb = {}; - - for(var o in rubberBBox) { - bb[o] = rubberBBox[o] / current_zoom; - } - rubberBBox = bb; - - } else { - var rubberBBox = rect; - } - var i = curBBoxes.length; - while (i--) { - if(!rubberBBox.width || !rubberBBox.width) continue; - if (svgedit.math.rectsIntersect(rubberBBox, curBBoxes[i].bbox)) { - resultList.push(curBBoxes[i].elem); - } - } - } - // addToSelection expects an array, but it's ok to pass a NodeList - // because using square-bracket notation is allowed: - // http://www.w3.org/TR/DOM-Level-2-Core/ecma-script-binding.html - return resultList; -}; - -// TODO(codedread): Migrate this into svgutils.js -// Function: getStrokedBBox -// Get the bounding box for one or more stroked and/or transformed elements -// -// Parameters: -// elems - Array with DOM elements to check -// -// Returns: -// A single bounding box object -getStrokedBBox = this.getStrokedBBox = function(elems) { - if(!elems) elems = getVisibleElements(); - if(!elems.length) return false; - // Make sure the expected BBox is returned if the element is a group - var getCheckedBBox = function(elem) { - - try { - // TODO: Fix issue with rotated groups. Currently they work - // fine in FF, but not in other browsers (same problem mentioned - // in Issue 339 comment #2). - - var bb = svgedit.utilities.getBBox(elem); - - var angle = svgedit.utilities.getRotationAngle(elem); - if ((angle && angle % 90) || - svgedit.math.hasMatrixTransform(svgedit.transformlist.getTransformList(elem))) { - // Accurate way to get BBox of rotated element in Firefox: - // Put element in group and get its BBox - - var good_bb = false; - - // Get the BBox from the raw path for these elements - var elemNames = ['ellipse','path','line','polyline','polygon']; - if(elemNames.indexOf(elem.tagName) >= 0) { - bb = good_bb = canvas.convertToPath(elem, true); - } else if(elem.tagName == 'rect') { - // Look for radius - var rx = elem.getAttribute('rx'); - var ry = elem.getAttribute('ry'); - if(rx || ry) { - bb = good_bb = canvas.convertToPath(elem, true); - } - } - - if(!good_bb) { - // Must use clone else FF freaks out - var clone = elem.cloneNode(true); - var g = document.createElementNS(svgns, "g"); - var parent = elem.parentNode; - parent.appendChild(g); - g.appendChild(clone); - bb = svgedit.utilities.bboxToObj(g.getBBox()); - parent.removeChild(g); - } - - - // Old method: Works by giving the rotated BBox, - // this is (unfortunately) what Opera and Safari do - // natively when getting the BBox of the parent group -// var angle = angle * Math.PI / 180.0; -// var rminx = Number.MAX_VALUE, rminy = Number.MAX_VALUE, -// rmaxx = Number.MIN_VALUE, rmaxy = Number.MIN_VALUE; -// var cx = round(bb.x + bb.width/2), -// cy = round(bb.y + bb.height/2); -// var pts = [ [bb.x - cx, bb.y - cy], -// [bb.x + bb.width - cx, bb.y - cy], -// [bb.x + bb.width - cx, bb.y + bb.height - cy], -// [bb.x - cx, bb.y + bb.height - cy] ]; -// var j = 4; -// while (j--) { -// var x = pts[j][0], -// y = pts[j][1], -// r = Math.sqrt( x*x + y*y ); -// var theta = Math.atan2(y,x) + angle; -// x = round(r * Math.cos(theta) + cx); -// y = round(r * Math.sin(theta) + cy); -// -// // now set the bbox for the shape after it's been rotated -// if (x < rminx) rminx = x; -// if (y < rminy) rminy = y; -// if (x > rmaxx) rmaxx = x; -// if (y > rmaxy) rmaxy = y; -// } -// -// bb.x = rminx; -// bb.y = rminy; -// bb.width = rmaxx - rminx; -// bb.height = rmaxy - rminy; - } - return bb; - } catch(e) { - console.log(elem, e); - return null; - } - }; - - var full_bb; - $.each(elems, function() { - if(full_bb) return; - if(!this.parentNode) return; - full_bb = getCheckedBBox(this); - }); - - // This shouldn't ever happen... - if(full_bb == null) return null; - - // full_bb doesn't include the stoke, so this does no good! -// if(elems.length == 1) return full_bb; - - var max_x = full_bb.x + full_bb.width; - var max_y = full_bb.y + full_bb.height; - var min_x = full_bb.x; - var min_y = full_bb.y; - - // FIXME: same re-creation problem with this function as getCheckedBBox() above - var getOffset = function(elem) { - var sw = elem.getAttribute("stroke-width"); - var offset = 0; - if (elem.getAttribute("stroke") != "none" && !isNaN(sw)) { - offset += sw/2; - } - return offset; - } - var bboxes = []; - $.each(elems, function(i, elem) { - var cur_bb = getCheckedBBox(elem); - if(cur_bb) { - var offset = getOffset(elem); - min_x = Math.min(min_x, cur_bb.x - offset); - min_y = Math.min(min_y, cur_bb.y - offset); - bboxes.push(cur_bb); - } - }); - - full_bb.x = min_x; - full_bb.y = min_y; - - $.each(elems, function(i, elem) { - var cur_bb = bboxes[i]; - // ensure that elem is really an element node - if (cur_bb && elem.nodeType == 1) { - var offset = getOffset(elem); - max_x = Math.max(max_x, cur_bb.x + cur_bb.width + offset); - max_y = Math.max(max_y, cur_bb.y + cur_bb.height + offset); - } - }); - - full_bb.width = max_x - min_x; - full_bb.height = max_y - min_y; - return full_bb; -} - -// Function: getVisibleElements -// Get all elements that have a BBox (excludes <defs>, <title>, etc). -// Note that 0-opacity, off-screen etc elements are still considered "visible" -// for this function -// -// Parameters: -// parent - The parent DOM element to search within -// -// Returns: -// An array with all "visible" elements. -var getVisibleElements = this.getVisibleElements = function(parent) { - if(!parent) parent = $(svgcontent).children(); // Prevent layers from being included - - var contentElems = []; - $(parent).children().each(function(i, elem) { - try { - if (elem.getBBox()) { - contentElems.push(elem); - } - } catch(e) {} - }); - return contentElems.reverse(); -}; - -// Function: getVisibleElementsAndBBoxes -// Get all elements that have a BBox (excludes <defs>, <title>, etc). -// Note that 0-opacity, off-screen etc elements are still considered "visible" -// for this function -// -// Parameters: -// parent - The parent DOM element to search within -// -// Returns: -// An array with objects that include: -// * elem - The element -// * bbox - The element's BBox as retrieved from getStrokedBBox -var getVisibleElementsAndBBoxes = this.getVisibleElementsAndBBoxes = function(parent) { - if(!parent) parent = $(svgcontent).children(); // Prevent layers from being included - - var contentElems = []; - $(parent).children().each(function(i, elem) { - try { - if (elem.getBBox()) { - contentElems.push({'elem':elem, 'bbox':getStrokedBBox([elem])}); - } - } catch(e) {} - }); - return contentElems.reverse(); -}; - -// Function: groupSvgElem -// Wrap an SVG element into a group element, mark the group as 'gsvg' -// -// Parameters: -// elem - SVG element to wrap -var groupSvgElem = this.groupSvgElem = function(elem) { - var g = document.createElementNS(svgns, "g"); - elem.parentNode.replaceChild(g, elem); - $(g).append(elem).data('gsvg', elem)[0].id = getNextId(); -} - -// Function: copyElem -// Create a clone of an element, updating its ID and its children's IDs when needed -// -// Parameters: -// el - DOM element to clone -// -// Returns: The cloned element -var copyElem = function(el) { - // manually create a copy of the element - var new_el = document.createElementNS(el.namespaceURI, el.nodeName); - $.each(el.attributes, function(i, attr) { - if (attr.localName != '-moz-math-font-style') { - new_el.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.nodeValue); - } - }); - // set the copied element's new id - new_el.removeAttribute("id"); - new_el.id = getNextId(); - - // Opera's "d" value needs to be reset for Opera/Win/non-EN - // Also needed for webkit (else does not keep curved segments on clone) - if(svgedit.browser.isWebkit() && el.nodeName == 'path') { - var fixed_d = pathActions.convertPath(el); - new_el.setAttribute('d', fixed_d); - } - - // now create copies of all children - $.each(el.childNodes, function(i, child) { - switch(child.nodeType) { - case 1: // element node - new_el.appendChild(copyElem(child)); - break; - case 3: // text node - new_el.textContent = child.nodeValue; - break; - default: - break; - } - }); - - if($(el).data('gsvg')) { - $(new_el).data('gsvg', new_el.firstChild); - } else if($(el).data('symbol')) { - var ref = $(el).data('symbol'); - $(new_el).data('ref', ref).data('symbol', ref); - } - - else if(new_el.tagName == 'image') { - preventClickDefault(new_el); - } - return new_el; -}; - -// Set scope for these functions -var getId, getNextId, call; - -(function(c) { - - // Object to contain editor event names and callback functions - var events = {}; - - getId = c.getId = function() { return getCurrentDrawing().getId(); }; - getNextId = c.getNextId = function() { return getCurrentDrawing().getNextId(); }; - - // Function: call - // Run the callback function associated with the given event - // - // Parameters: - // event - String with the event name - // arg - Argument to pass through to the callback function - call = c.call = function(event, arg) { - if (events[event]) { - return events[event](this, arg); - } - }; - - // Function: bind - // Attaches a callback function to an event - // - // Parameters: - // event - String indicating the name of the event - // f - The callback function to bind to the event - // - // Return: - // The previous event - c.bind = function(event, f) { - var old = events[event]; - events[event] = f; - return old; - }; - -}(canvas)); - -// Function: canvas.prepareSvg -// Runs the SVG Document through the sanitizer and then updates its paths. -// -// Parameters: -// newDoc - The SVG DOM document -this.prepareSvg = function(newDoc) { - this.sanitizeSvg(newDoc.documentElement); - - // convert paths into absolute commands - var paths = newDoc.getElementsByTagNameNS(svgns, "path"); - for (var i = 0, len = paths.length; i < len; ++i) { - var path = paths[i]; - path.setAttribute('d', pathActions.convertPath(path)); - pathActions.fixEnd(path); - } -}; - -// Function getRefElem -// Get the reference element associated with the given attribute value -// -// Parameters: -// attrVal - The attribute value as a string -var getRefElem = this.getRefElem = function(attrVal) { - return getElem(getUrlFromAttr(attrVal).substr(1)); -} - -// Function: ffClone -// Hack for Firefox bugs where text element features aren't updated or get -// messed up. See issue 136 and issue 137. -// This function clones the element and re-selects it -// TODO: Test for this bug on load and add it to "support" object instead of -// browser sniffing -// -// Parameters: -// elem - The (text) DOM element to clone -var ffClone = function(elem) { - if(!svgedit.browser.isGecko()) return elem; - var clone = elem.cloneNode(true) - elem.parentNode.insertBefore(clone, elem); - elem.parentNode.removeChild(elem); - selectorManager.releaseSelector(elem); - selectedElements[0] = clone; - selectorManager.requestSelector(clone).showGrips(true); - return clone; -} - - -// this.each is deprecated, if any extension used this it can be recreated by doing this: -// $(canvas.getRootElem()).children().each(...) - -// this.each = function(cb) { -// $(svgroot).children().each(cb); -// }; - - -// Function: setRotationAngle -// Removes any old rotations if present, prepends a new rotation at the -// transformed center -// -// Parameters: -// val - The new rotation angle in degrees -// preventUndo - Boolean indicating whether the action should be undoable or not -this.setRotationAngle = function(val, preventUndo) { - // ensure val is the proper type - val = parseFloat(val); - var elem = selectedElements[0]; - var oldTransform = elem.getAttribute("transform"); - var bbox = svgedit.utilities.getBBox(elem); - var cx = bbox.x+bbox.width/2, cy = bbox.y+bbox.height/2; - var tlist = getTransformList(elem); - - // only remove the real rotational transform if present (i.e. at index=0) - if (tlist.numberOfItems > 0) { - var xform = tlist.getItem(0); - if (xform.type == 4) { - tlist.removeItem(0); - } - } - // find R_nc and insert it - if (val != 0) { - var center = transformPoint(cx,cy,transformListToTransform(tlist).matrix); - var R_nc = svgroot.createSVGTransform(); - R_nc.setRotate(val, center.x, center.y); - if(tlist.numberOfItems) { - tlist.insertItemBefore(R_nc, 0); - } else { - tlist.appendItem(R_nc); - } - } - else if (tlist.numberOfItems == 0) { - elem.removeAttribute("transform"); - } - - if (!preventUndo) { - // we need to undo it, then redo it so it can be undo-able! :) - // TODO: figure out how to make changes to transform list undo-able cross-browser? - var newTransform = elem.getAttribute("transform"); - elem.setAttribute("transform", oldTransform); - changeSelectedAttribute("transform",newTransform,selectedElements); - call("changed", selectedElements); - } - var pointGripContainer = getElem("pathpointgrip_container"); -// if(elem.nodeName == "path" && pointGripContainer) { -// pathActions.setPointContainerTransform(elem.getAttribute("transform")); -// } - var selector = selectorManager.requestSelector(selectedElements[0]); - selector.resize(); - selector.updateGripCursors(val); -}; - -// Function: recalculateAllSelectedDimensions -// Runs recalculateDimensions on the selected elements, -// adding the changes to a single batch command -var recalculateAllSelectedDimensions = this.recalculateAllSelectedDimensions = function() { - var text = (current_resize_mode == "none" ? "position" : "size"); - var batchCmd = new BatchCommand(text); - - var i = selectedElements.length; - while(i--) { - var elem = selectedElements[i]; -// if(getRotationAngle(elem) && !hasMatrixTransform(getTransformList(elem))) continue; - var cmd = recalculateDimensions(elem); - if (cmd) { - batchCmd.addSubCommand(cmd); - } - } - - if (!batchCmd.isEmpty()) { - addCommandToHistory(batchCmd); - call("changed", selectedElements); - } -}; - -// this is how we map paths to our preferred relative segment types -var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a', - 'H', 'h', 'V', 'v', 'S', 's', 'T', 't']; - -// Debug tool to easily see the current matrix in the browser's console -var logMatrix = function(m) { - console.log([m.a,m.b,m.c,m.d,m.e,m.f]); -}; - -// Function: remapElement -// Applies coordinate changes to an element based on the given matrix -// -// Parameters: -// selected - DOM element to be changed -// changes - Object with changes to be remapped -// m - Matrix object to use for remapping coordinates -var remapElement = this.remapElement = function(selected,changes,m) { - - var remap = function(x,y) { return transformPoint(x,y,m); }, - scalew = function(w) { return m.a*w; }, - scaleh = function(h) { return m.d*h; }, - doSnapping = curConfig.gridSnapping && selected.parentNode.parentNode.localName === "svg", - finishUp = function() { - if(doSnapping) for(var o in changes) changes[o] = snapToGrid(changes[o]); - assignAttributes(selected, changes, 1000, true); - } - box = svgedit.utilities.getBBox(selected); - - for(var i = 0; i < 2; i++) { - var type = i === 0 ? 'fill' : 'stroke'; - var attrVal = selected.getAttribute(type); - if(attrVal && attrVal.indexOf('url(') === 0) { - if(m.a < 0 || m.d < 0) { - var grad = getRefElem(attrVal); - var newgrad = grad.cloneNode(true); - - if(m.a < 0) { - //flip x - var x1 = newgrad.getAttribute('x1'); - var x2 = newgrad.getAttribute('x2'); - newgrad.setAttribute('x1', -(x1 - 1)); - newgrad.setAttribute('x2', -(x2 - 1)); - } - - if(m.d < 0) { - //flip y - var y1 = newgrad.getAttribute('y1'); - var y2 = newgrad.getAttribute('y2'); - newgrad.setAttribute('y1', -(y1 - 1)); - newgrad.setAttribute('y2', -(y2 - 1)); - } - newgrad.id = getNextId(); - findDefs().appendChild(newgrad); - selected.setAttribute(type, 'url(#' + newgrad.id + ')'); - } - - // Not really working :( -// if(selected.tagName === 'path') { -// reorientGrads(selected, m); -// } - } - } - - - var elName = selected.tagName; - if(elName === "g" || elName === "text" || elName === "use") { - // if it was a translate, then just update x,y - if (m.a == 1 && m.b == 0 && m.c == 0 && m.d == 1 && - (m.e != 0 || m.f != 0) ) - { - // [T][M] = [M][T'] - // therefore [T'] = [M_inv][T][M] - var existing = transformListToTransform(selected).matrix, - t_new = matrixMultiply(existing.inverse(), m, existing); - changes.x = parseFloat(changes.x) + t_new.e; - changes.y = parseFloat(changes.y) + t_new.f; - } - else { - // we just absorb all matrices into the element and don't do any remapping - var chlist = getTransformList(selected); - var mt = svgroot.createSVGTransform(); - mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix,m)); - chlist.clear(); - chlist.appendItem(mt); - } - } - - // now we have a set of changes and an applied reduced transform list - // we apply the changes directly to the DOM - switch (elName) - { - case "foreignObject": - case "rect": - case "image": - - // Allow images to be inverted (give them matrix when flipped) - if(elName === 'image' && (m.a < 0 || m.d < 0)) { - // Convert to matrix - var chlist = getTransformList(selected); - var mt = svgroot.createSVGTransform(); - mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix,m)); - chlist.clear(); - chlist.appendItem(mt); - } else { - var pt1 = remap(changes.x,changes.y); - - changes.width = scalew(changes.width); - changes.height = scaleh(changes.height); - - changes.x = pt1.x + Math.min(0,changes.width); - changes.y = pt1.y + Math.min(0,changes.height); - changes.width = Math.abs(changes.width); - changes.height = Math.abs(changes.height); - } - finishUp(); - break; - case "ellipse": - var c = remap(changes.cx,changes.cy); - changes.cx = c.x; - changes.cy = c.y; - changes.rx = scalew(changes.rx); - changes.ry = scaleh(changes.ry); - - changes.rx = Math.abs(changes.rx); - changes.ry = Math.abs(changes.ry); - finishUp(); - break; - case "circle": - var c = remap(changes.cx,changes.cy); - changes.cx = c.x; - changes.cy = c.y; - // take the minimum of the new selected box's dimensions for the new circle radius - var tbox = svgedit.math.transformBox(box.x, box.y, box.width, box.height, m); - var w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y; - changes.r = Math.min(w/2, h/2); - - if(changes.r) changes.r = Math.abs(changes.r); - finishUp(); - break; - case "line": - var pt1 = remap(changes.x1,changes.y1), - pt2 = remap(changes.x2,changes.y2); - changes.x1 = pt1.x; - changes.y1 = pt1.y; - changes.x2 = pt2.x; - changes.y2 = pt2.y; - - case "text": - case "use": - finishUp(); - break; - case "g": - var gsvg = $(selected).data('gsvg'); - if(gsvg) { - assignAttributes(gsvg, changes, 1000, true); - } - break; - case "polyline": - case "polygon": - var len = changes.points.length; - for (var i = 0; i < len; ++i) { - var pt = changes.points[i]; - pt = remap(pt.x,pt.y); - changes.points[i].x = pt.x; - changes.points[i].y = pt.y; - } - - var len = changes.points.length; - var pstr = ""; - for (var i = 0; i < len; ++i) { - var pt = changes.points[i]; - pstr += pt.x + "," + pt.y + " "; - } - selected.setAttribute("points", pstr); - break; - case "path": - - var segList = selected.pathSegList; - var len = segList.numberOfItems; - changes.d = new Array(len); - for (var i = 0; i < len; ++i) { - var seg = segList.getItem(i); - changes.d[i] = { - type: seg.pathSegType, - x: seg.x, - y: seg.y, - x1: seg.x1, - y1: seg.y1, - x2: seg.x2, - y2: seg.y2, - r1: seg.r1, - r2: seg.r2, - angle: seg.angle, - largeArcFlag: seg.largeArcFlag, - sweepFlag: seg.sweepFlag - }; - } - - var len = changes.d.length, - firstseg = changes.d[0], - currentpt = remap(firstseg.x,firstseg.y); - changes.d[0].x = currentpt.x; - changes.d[0].y = currentpt.y; - for (var i = 1; i < len; ++i) { - var seg = changes.d[i]; - var type = seg.type; - // if absolute or first segment, we want to remap x, y, x1, y1, x2, y2 - // if relative, we want to scalew, scaleh - if (type % 2 == 0) { // absolute - var thisx = (seg.x != undefined) ? seg.x : currentpt.x, // for V commands - thisy = (seg.y != undefined) ? seg.y : currentpt.y, // for H commands - pt = remap(thisx,thisy), - pt1 = remap(seg.x1,seg.y1), - pt2 = remap(seg.x2,seg.y2); - seg.x = pt.x; - seg.y = pt.y; - seg.x1 = pt1.x; - seg.y1 = pt1.y; - seg.x2 = pt2.x; - seg.y2 = pt2.y; - seg.r1 = scalew(seg.r1), - seg.r2 = scaleh(seg.r2); - } - else { // relative - seg.x = scalew(seg.x); - seg.y = scaleh(seg.y); - seg.x1 = scalew(seg.x1); - seg.y1 = scaleh(seg.y1); - seg.x2 = scalew(seg.x2); - seg.y2 = scaleh(seg.y2); - seg.r1 = scalew(seg.r1), - seg.r2 = scaleh(seg.r2); - } - } // for each segment - - var dstr = ""; - var len = changes.d.length; - for (var i = 0; i < len; ++i) { - var seg = changes.d[i]; - var type = seg.type; - dstr += pathMap[type]; - switch(type) { - case 13: // relative horizontal line (h) - case 12: // absolute horizontal line (H) - dstr += seg.x + " "; - break; - case 15: // relative vertical line (v) - case 14: // absolute vertical line (V) - dstr += seg.y + " "; - break; - case 3: // relative move (m) - case 5: // relative line (l) - case 19: // relative smooth quad (t) - case 2: // absolute move (M) - case 4: // absolute line (L) - case 18: // absolute smooth quad (T) - dstr += seg.x + "," + seg.y + " "; - break; - case 7: // relative cubic (c) - case 6: // absolute cubic (C) - dstr += seg.x1 + "," + seg.y1 + " " + seg.x2 + "," + seg.y2 + " " + - seg.x + "," + seg.y + " "; - break; - case 9: // relative quad (q) - case 8: // absolute quad (Q) - dstr += seg.x1 + "," + seg.y1 + " " + seg.x + "," + seg.y + " "; - break; - case 11: // relative elliptical arc (a) - case 10: // absolute elliptical arc (A) - dstr += seg.r1 + "," + seg.r2 + " " + seg.angle + " " + (+seg.largeArcFlag) + - " " + (+seg.sweepFlag) + " " + seg.x + "," + seg.y + " "; - break; - case 17: // relative smooth cubic (s) - case 16: // absolute smooth cubic (S) - dstr += seg.x2 + "," + seg.y2 + " " + seg.x + "," + seg.y + " "; - break; - } - } - - selected.setAttribute("d", dstr); - break; - } -}; - -// Function: updateClipPath -// Updates a <clipPath>s values based on the given translation of an element -// -// Parameters: -// attr - The clip-path attribute value with the clipPath's ID -// tx - The translation's x value -// ty - The translation's y value -var updateClipPath = function(attr, tx, ty) { - var path = getRefElem(attr).firstChild; - - var cp_xform = getTransformList(path); - - var newxlate = svgroot.createSVGTransform(); - newxlate.setTranslate(tx, ty); - - cp_xform.appendItem(newxlate); - - // Update clipPath's dimensions - recalculateDimensions(path); -} - -// Function: recalculateDimensions -// Decides the course of action based on the element's transform list -// -// Parameters: -// selected - The DOM element to recalculate -// -// Returns: -// Undo command object with the resulting change -var recalculateDimensions = this.recalculateDimensions = function(selected) { - if (selected == null) return null; - - var tlist = getTransformList(selected); - - // remove any unnecessary transforms - if (tlist && tlist.numberOfItems > 0) { - var k = tlist.numberOfItems; - while (k--) { - var xform = tlist.getItem(k); - if (xform.type === 0) { - tlist.removeItem(k); - } - // remove identity matrices - else if (xform.type === 1) { - if (svgedit.math.isIdentity(xform.matrix)) { - tlist.removeItem(k); - } - } - // remove zero-degree rotations - else if (xform.type === 4) { - if (xform.angle === 0) { - tlist.removeItem(k); - } - } - } - // End here if all it has is a rotation - if(tlist.numberOfItems === 1 && getRotationAngle(selected)) return null; - } - - // if this element had no transforms, we are done - if (!tlist || tlist.numberOfItems == 0) { - selected.removeAttribute("transform"); - return null; - } - - // TODO: Make this work for more than 2 - if (tlist) { - var k = tlist.numberOfItems; - var mxs = []; - while (k--) { - var xform = tlist.getItem(k); - if (xform.type === 1) { - mxs.push([xform.matrix, k]); - } else if(mxs.length) { - mxs = []; - } - } - if(mxs.length === 2) { - var m_new = svgroot.createSVGTransformFromMatrix(matrixMultiply(mxs[1][0], mxs[0][0])); - tlist.removeItem(mxs[0][1]); - tlist.removeItem(mxs[1][1]); - tlist.insertItemBefore(m_new, mxs[1][1]); - } - - // combine matrix + translate - k = tlist.numberOfItems; - if(k >= 2 && tlist.getItem(k-2).type === 1 && tlist.getItem(k-1).type === 2) { - var mt = svgroot.createSVGTransform(); - - var m = matrixMultiply( - tlist.getItem(k-2).matrix, - tlist.getItem(k-1).matrix - ); - mt.setMatrix(m); - tlist.removeItem(k-2); - tlist.removeItem(k-2); - tlist.appendItem(mt); - } - } - - // If it still has a single [M] or [R][M], return null too (prevents BatchCommand from being returned). - switch ( selected.tagName ) { - // Ignore these elements, as they can absorb the [M] - case 'line': - case 'polyline': - case 'polygon': - case 'path': - break; - default: - if( - (tlist.numberOfItems === 1 && tlist.getItem(0).type === 1) - || (tlist.numberOfItems === 2 && tlist.getItem(0).type === 1 && tlist.getItem(0).type === 4) - ) { - return null; - } - } - - // Grouped SVG element - var gsvg = $(selected).data('gsvg'); - - // we know we have some transforms, so set up return variable - var batchCmd = new BatchCommand("Transform"); - - // store initial values that will be affected by reducing the transform list - var changes = {}, initial = null, attrs = []; - switch (selected.tagName) - { - case "line": - attrs = ["x1", "y1", "x2", "y2"]; - break; - case "circle": - attrs = ["cx", "cy", "r"]; - break; - case "ellipse": - attrs = ["cx", "cy", "rx", "ry"]; - break; - case "foreignObject": - case "rect": - case "image": - attrs = ["width", "height", "x", "y"]; - break; - case "use": - case "text": - attrs = ["x", "y"]; - break; - case "polygon": - case "polyline": - initial = {}; - initial["points"] = selected.getAttribute("points"); - var list = selected.points; - var len = list.numberOfItems; - changes["points"] = new Array(len); - for (var i = 0; i < len; ++i) { - var pt = list.getItem(i); - changes["points"][i] = {x:pt.x,y:pt.y}; - } - break; - case "path": - initial = {}; - initial["d"] = selected.getAttribute("d"); - changes["d"] = selected.getAttribute("d"); - break; - } // switch on element type to get initial values - - if(attrs.length) { - changes = $(selected).attr(attrs); - $.each(changes, function(attr, val) { - changes[attr] = convertToNum(attr, val); - }); - } else if(gsvg) { - // GSVG exception - changes = { - x: $(gsvg).attr('x') || 0, - y: $(gsvg).attr('y') || 0 - }; - } - - // if we haven't created an initial array in polygon/polyline/path, then - // make a copy of initial values and include the transform - if (initial == null) { - initial = $.extend(true, {}, changes); - $.each(initial, function(attr, val) { - initial[attr] = convertToNum(attr, val); - }); - } - // save the start transform value too - initial["transform"] = start_transform ? start_transform : ""; - - // if it's a regular group, we have special processing to flatten transforms - if ((selected.tagName == "g" && !gsvg) || selected.tagName == "a") { - var box = svgedit.utilities.getBBox(selected), - oldcenter = {x: box.x+box.width/2, y: box.y+box.height/2}, - newcenter = transformPoint(box.x+box.width/2, box.y+box.height/2, - transformListToTransform(tlist).matrix), - m = svgroot.createSVGMatrix(); - - - // temporarily strip off the rotate and save the old center - var gangle = getRotationAngle(selected); - if (gangle) { - var a = gangle * Math.PI / 180; - if ( Math.abs(a) > (1.0e-10) ) { - var s = Math.sin(a)/(1 - Math.cos(a)); - } else { - // FIXME: This blows up if the angle is exactly 0! - var s = 2/a; - } - for (var i = 0; i < tlist.numberOfItems; ++i) { - var xform = tlist.getItem(i); - if (xform.type == 4) { - // extract old center through mystical arts - var rm = xform.matrix; - oldcenter.y = (s*rm.e + rm.f)/2; - oldcenter.x = (rm.e - s*rm.f)/2; - tlist.removeItem(i); - break; - } - } - } - var tx = 0, ty = 0, - operation = 0, - N = tlist.numberOfItems; - - if(N) { - var first_m = tlist.getItem(0).matrix; - } - - // first, if it was a scale then the second-last transform will be it - if (N >= 3 && tlist.getItem(N-2).type == 3 && - tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2) - { - operation = 3; // scale - - // if the children are unrotated, pass the scale down directly - // otherwise pass the equivalent matrix() down directly - var tm = tlist.getItem(N-3).matrix, - sm = tlist.getItem(N-2).matrix, - tmn = tlist.getItem(N-1).matrix; - - var children = selected.childNodes; - var c = children.length; - while (c--) { - var child = children.item(c); - tx = 0; - ty = 0; - if (child.nodeType == 1) { - var childTlist = getTransformList(child); - - // some children might not have a transform (<metadata>, <defs>, etc) - if (!childTlist) continue; - - var m = transformListToTransform(childTlist).matrix; - - // Convert a matrix to a scale if applicable -// if(hasMatrixTransform(childTlist) && childTlist.numberOfItems == 1) { -// if(m.b==0 && m.c==0 && m.e==0 && m.f==0) { -// childTlist.removeItem(0); -// var translateOrigin = svgroot.createSVGTransform(), -// scale = svgroot.createSVGTransform(), -// translateBack = svgroot.createSVGTransform(); -// translateOrigin.setTranslate(0, 0); -// scale.setScale(m.a, m.d); -// translateBack.setTranslate(0, 0); -// childTlist.appendItem(translateBack); -// childTlist.appendItem(scale); -// childTlist.appendItem(translateOrigin); -// } -// } - - var angle = getRotationAngle(child); - var old_start_transform = start_transform; - var childxforms = []; - start_transform = child.getAttribute("transform"); - if(angle || hasMatrixTransform(childTlist)) { - var e2t = svgroot.createSVGTransform(); - e2t.setMatrix(matrixMultiply(tm, sm, tmn, m)); - childTlist.clear(); - childTlist.appendItem(e2t); - childxforms.push(e2t); - } - // if not rotated or skewed, push the [T][S][-T] down to the child - else { - // update the transform list with translate,scale,translate - - // slide the [T][S][-T] from the front to the back - // [T][S][-T][M] = [M][T2][S2][-T2] - - // (only bringing [-T] to the right of [M]) - // [T][S][-T][M] = [T][S][M][-T2] - // [-T2] = [M_inv][-T][M] - var t2n = matrixMultiply(m.inverse(), tmn, m); - // [T2] is always negative translation of [-T2] - var t2 = svgroot.createSVGMatrix(); - t2.e = -t2n.e; - t2.f = -t2n.f; - - // [T][S][-T][M] = [M][T2][S2][-T2] - // [S2] = [T2_inv][M_inv][T][S][-T][M][-T2_inv] - var s2 = matrixMultiply(t2.inverse(), m.inverse(), tm, sm, tmn, m, t2n.inverse()); - - var translateOrigin = svgroot.createSVGTransform(), - scale = svgroot.createSVGTransform(), - translateBack = svgroot.createSVGTransform(); - translateOrigin.setTranslate(t2n.e, t2n.f); - scale.setScale(s2.a, s2.d); - translateBack.setTranslate(t2.e, t2.f); - childTlist.appendItem(translateBack); - childTlist.appendItem(scale); - childTlist.appendItem(translateOrigin); - childxforms.push(translateBack); - childxforms.push(scale); - childxforms.push(translateOrigin); -// logMatrix(translateBack.matrix); -// logMatrix(scale.matrix); - } // not rotated - batchCmd.addSubCommand( recalculateDimensions(child) ); - // TODO: If any <use> have this group as a parent and are - // referencing this child, then we need to impose a reverse - // scale on it so that when it won't get double-translated -// var uses = selected.getElementsByTagNameNS(svgns, "use"); -// var href = "#"+child.id; -// var u = uses.length; -// while (u--) { -// var useElem = uses.item(u); -// if(href == getHref(useElem)) { -// var usexlate = svgroot.createSVGTransform(); -// usexlate.setTranslate(-tx,-ty); -// getTransformList(useElem).insertItemBefore(usexlate,0); -// batchCmd.addSubCommand( recalculateDimensions(useElem) ); -// } -// } - start_transform = old_start_transform; - } // element - } // for each child - // Remove these transforms from group - tlist.removeItem(N-1); - tlist.removeItem(N-2); - tlist.removeItem(N-3); - } - else if (N >= 3 && tlist.getItem(N-1).type == 1) - { - operation = 3; // scale - m = transformListToTransform(tlist).matrix; - var e2t = svgroot.createSVGTransform(); - e2t.setMatrix(m); - tlist.clear(); - tlist.appendItem(e2t); - } - // next, check if the first transform was a translate - // if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ] - // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] - else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) && - tlist.getItem(0).type == 2) - { - operation = 2; // translate - var T_M = transformListToTransform(tlist).matrix; - tlist.removeItem(0); - var M_inv = transformListToTransform(tlist).matrix.inverse(); - var M2 = matrixMultiply( M_inv, T_M ); - - tx = M2.e; - ty = M2.f; - - if (tx != 0 || ty != 0) { - // we pass the translates down to the individual children - var children = selected.childNodes; - var c = children.length; - - var clipPaths_done = []; - - while (c--) { - var child = children.item(c); - if (child.nodeType == 1) { - - // Check if child has clip-path - if(child.getAttribute('clip-path')) { - // tx, ty - var attr = child.getAttribute('clip-path'); - if(clipPaths_done.indexOf(attr) === -1) { - updateClipPath(attr, tx, ty); - clipPaths_done.push(attr); - } - } - - var old_start_transform = start_transform; - start_transform = child.getAttribute("transform"); - - var childTlist = getTransformList(child); - // some children might not have a transform (<metadata>, <defs>, etc) - if (childTlist) { - var newxlate = svgroot.createSVGTransform(); - newxlate.setTranslate(tx,ty); - if(childTlist.numberOfItems) { - childTlist.insertItemBefore(newxlate, 0); - } else { - childTlist.appendItem(newxlate); - } - batchCmd.addSubCommand( recalculateDimensions(child) ); - // If any <use> have this group as a parent and are - // referencing this child, then impose a reverse translate on it - // so that when it won't get double-translated - var uses = selected.getElementsByTagNameNS(svgns, "use"); - var href = "#"+child.id; - var u = uses.length; - while (u--) { - var useElem = uses.item(u); - if(href == getHref(useElem)) { - var usexlate = svgroot.createSVGTransform(); - usexlate.setTranslate(-tx,-ty); - getTransformList(useElem).insertItemBefore(usexlate,0); - batchCmd.addSubCommand( recalculateDimensions(useElem) ); - } - } - start_transform = old_start_transform; - } - } - } - - clipPaths_done = []; - - start_transform = old_start_transform; - } - } - // else, a matrix imposition from a parent group - // keep pushing it down to the children - else if (N == 1 && tlist.getItem(0).type == 1 && !gangle) { - operation = 1; - var m = tlist.getItem(0).matrix, - children = selected.childNodes, - c = children.length; - while (c--) { - var child = children.item(c); - if (child.nodeType == 1) { - var old_start_transform = start_transform; - start_transform = child.getAttribute("transform"); - var childTlist = getTransformList(child); - - if (!childTlist) continue; - - var em = matrixMultiply(m, transformListToTransform(childTlist).matrix); - var e2m = svgroot.createSVGTransform(); - e2m.setMatrix(em); - childTlist.clear(); - childTlist.appendItem(e2m,0); - - batchCmd.addSubCommand( recalculateDimensions(child) ); - start_transform = old_start_transform; - - // Convert stroke - // TODO: Find out if this should actually happen somewhere else - var sw = child.getAttribute("stroke-width"); - if (child.getAttribute("stroke") !== "none" && !isNaN(sw)) { - var avg = (Math.abs(em.a) + Math.abs(em.d)) / 2; - child.setAttribute('stroke-width', sw * avg); - } - - } - } - tlist.clear(); - } - // else it was just a rotate - else { - if (gangle) { - var newRot = svgroot.createSVGTransform(); - newRot.setRotate(gangle,newcenter.x,newcenter.y); - if(tlist.numberOfItems) { - tlist.insertItemBefore(newRot, 0); - } else { - tlist.appendItem(newRot); - } - } - if (tlist.numberOfItems == 0) { - selected.removeAttribute("transform"); - } - return null; - } - - // if it was a translate, put back the rotate at the new center - if (operation == 2) { - if (gangle) { - newcenter = { - x: oldcenter.x + first_m.e, - y: oldcenter.y + first_m.f - }; - - var newRot = svgroot.createSVGTransform(); - newRot.setRotate(gangle,newcenter.x,newcenter.y); - if(tlist.numberOfItems) { - tlist.insertItemBefore(newRot, 0); - } else { - tlist.appendItem(newRot); - } - } - } - // if it was a resize - else if (operation == 3) { - var m = transformListToTransform(tlist).matrix; - var roldt = svgroot.createSVGTransform(); - roldt.setRotate(gangle, oldcenter.x, oldcenter.y); - var rold = roldt.matrix; - var rnew = svgroot.createSVGTransform(); - rnew.setRotate(gangle, newcenter.x, newcenter.y); - var rnew_inv = rnew.matrix.inverse(), - m_inv = m.inverse(), - extrat = matrixMultiply(m_inv, rnew_inv, rold, m); - - tx = extrat.e; - ty = extrat.f; - - if (tx != 0 || ty != 0) { - // now push this transform down to the children - // we pass the translates down to the individual children - var children = selected.childNodes; - var c = children.length; - while (c--) { - var child = children.item(c); - if (child.nodeType == 1) { - var old_start_transform = start_transform; - start_transform = child.getAttribute("transform"); - var childTlist = getTransformList(child); - var newxlate = svgroot.createSVGTransform(); - newxlate.setTranslate(tx,ty); - if(childTlist.numberOfItems) { - childTlist.insertItemBefore(newxlate, 0); - } else { - childTlist.appendItem(newxlate); - } - - batchCmd.addSubCommand( recalculateDimensions(child) ); - start_transform = old_start_transform; - } - } - } - - if (gangle) { - if(tlist.numberOfItems) { - tlist.insertItemBefore(rnew, 0); - } else { - tlist.appendItem(rnew); - } - } - } - } - // else, it's a non-group - else { - - // FIXME: box might be null for some elements (<metadata> etc), need to handle this - var box = svgedit.utilities.getBBox(selected); - - // Paths (and possbly other shapes) will have no BBox while still in <defs>, - // but we still may need to recalculate them (see issue 595). - // TODO: Figure out how to get BBox from these elements in case they - // have a rotation transform - - if(!box && selected.tagName != 'path') return null; - - - var m = svgroot.createSVGMatrix(), - // temporarily strip off the rotate and save the old center - angle = getRotationAngle(selected); - if (angle) { - var oldcenter = {x: box.x+box.width/2, y: box.y+box.height/2}, - newcenter = transformPoint(box.x+box.width/2, box.y+box.height/2, - transformListToTransform(tlist).matrix); - - var a = angle * Math.PI / 180; - if ( Math.abs(a) > (1.0e-10) ) { - var s = Math.sin(a)/(1 - Math.cos(a)); - } else { - // FIXME: This blows up if the angle is exactly 0! - var s = 2/a; - } - for (var i = 0; i < tlist.numberOfItems; ++i) { - var xform = tlist.getItem(i); - if (xform.type == 4) { - // extract old center through mystical arts - var rm = xform.matrix; - oldcenter.y = (s*rm.e + rm.f)/2; - oldcenter.x = (rm.e - s*rm.f)/2; - tlist.removeItem(i); - break; - } - } - } - - // 2 = translate, 3 = scale, 4 = rotate, 1 = matrix imposition - var operation = 0; - var N = tlist.numberOfItems; - - // Check if it has a gradient with userSpaceOnUse, in which case - // adjust it by recalculating the matrix transform. - // TODO: Make this work in Webkit using svgedit.transformlist.SVGTransformList - if(!svgedit.browser.isWebkit()) { - var fill = selected.getAttribute('fill'); - if(fill && fill.indexOf('url(') === 0) { - var paint = getRefElem(fill); - var type = 'pattern'; - if(paint.tagName !== type) type = 'gradient'; - var attrVal = paint.getAttribute(type + 'Units'); - if(attrVal === 'userSpaceOnUse') { - //Update the userSpaceOnUse element - m = transformListToTransform(tlist).matrix; - var gtlist = getTransformList(paint); - var gmatrix = transformListToTransform(gtlist).matrix; - m = matrixMultiply(m, gmatrix); - var m_str = "matrix(" + [m.a,m.b,m.c,m.d,m.e,m.f].join(",") + ")"; - paint.setAttribute(type + 'Transform', m_str); - } - } - } - - // first, if it was a scale of a non-skewed element, then the second-last - // transform will be the [S] - // if we had [M][T][S][T] we want to extract the matrix equivalent of - // [T][S][T] and push it down to the element - if (N >= 3 && tlist.getItem(N-2).type == 3 && - tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2) - - // Removed this so a <use> with a given [T][S][T] would convert to a matrix. - // Is that bad? - // && selected.nodeName != "use" - { - operation = 3; // scale - m = transformListToTransform(tlist,N-3,N-1).matrix; - tlist.removeItem(N-1); - tlist.removeItem(N-2); - tlist.removeItem(N-3); - } // if we had [T][S][-T][M], then this was a skewed element being resized - // Thus, we simply combine it all into one matrix - else if(N == 4 && tlist.getItem(N-1).type == 1) { - operation = 3; // scale - m = transformListToTransform(tlist).matrix; - var e2t = svgroot.createSVGTransform(); - e2t.setMatrix(m); - tlist.clear(); - tlist.appendItem(e2t); - // reset the matrix so that the element is not re-mapped - m = svgroot.createSVGMatrix(); - } // if we had [R][T][S][-T][M], then this was a rotated matrix-element - // if we had [T1][M] we want to transform this into [M][T2] - // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2] - // down to the element - else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) && - tlist.getItem(0).type == 2) - { - operation = 2; // translate - var oldxlate = tlist.getItem(0).matrix, - meq = transformListToTransform(tlist,1).matrix, - meq_inv = meq.inverse(); - m = matrixMultiply( meq_inv, oldxlate, meq ); - tlist.removeItem(0); - } - // else if this child now has a matrix imposition (from a parent group) - // we might be able to simplify - else if (N == 1 && tlist.getItem(0).type == 1 && !angle) { - // Remap all point-based elements - m = transformListToTransform(tlist).matrix; - switch (selected.tagName) { - case 'line': - changes = $(selected).attr(["x1","y1","x2","y2"]); - case 'polyline': - case 'polygon': - changes.points = selected.getAttribute("points"); - if(changes.points) { - var list = selected.points; - var len = list.numberOfItems; - changes.points = new Array(len); - for (var i = 0; i < len; ++i) { - var pt = list.getItem(i); - changes.points[i] = {x:pt.x,y:pt.y}; - } - } - case 'path': - changes.d = selected.getAttribute("d"); - operation = 1; - tlist.clear(); - break; - default: - break; - } - } - // if it was a rotation, put the rotate back and return without a command - // (this function has zero work to do for a rotate()) - else { - operation = 4; // rotation - if (angle) { - var newRot = svgroot.createSVGTransform(); - newRot.setRotate(angle,newcenter.x,newcenter.y); - - if(tlist.numberOfItems) { - tlist.insertItemBefore(newRot, 0); - } else { - tlist.appendItem(newRot); - } - } - if (tlist.numberOfItems == 0) { - selected.removeAttribute("transform"); - } - return null; - } - - // if it was a translate or resize, we need to remap the element and absorb the xform - if (operation == 1 || operation == 2 || operation == 3) { - remapElement(selected,changes,m); - } // if we are remapping - - // if it was a translate, put back the rotate at the new center - if (operation == 2) { - if (angle) { - if(!hasMatrixTransform(tlist)) { - newcenter = { - x: oldcenter.x + m.e, - y: oldcenter.y + m.f - }; - } - var newRot = svgroot.createSVGTransform(); - newRot.setRotate(angle, newcenter.x, newcenter.y); - if(tlist.numberOfItems) { - tlist.insertItemBefore(newRot, 0); - } else { - tlist.appendItem(newRot); - } - } - } - // [Rold][M][T][S][-T] became [Rold][M] - // we want it to be [Rnew][M][Tr] where Tr is the - // translation required to re-center it - // Therefore, [Tr] = [M_inv][Rnew_inv][Rold][M] - else if (operation == 3 && angle) { - var m = transformListToTransform(tlist).matrix; - var roldt = svgroot.createSVGTransform(); - roldt.setRotate(angle, oldcenter.x, oldcenter.y); - var rold = roldt.matrix; - var rnew = svgroot.createSVGTransform(); - rnew.setRotate(angle, newcenter.x, newcenter.y); - var rnew_inv = rnew.matrix.inverse(); - var m_inv = m.inverse(); - var extrat = matrixMultiply(m_inv, rnew_inv, rold, m); - - remapElement(selected,changes,extrat); - if (angle) { - if(tlist.numberOfItems) { - tlist.insertItemBefore(rnew, 0); - } else { - tlist.appendItem(rnew); - } - } - } - } // a non-group - - // if the transform list has been emptied, remove it - if (tlist.numberOfItems == 0) { - selected.removeAttribute("transform"); - } - - batchCmd.addSubCommand(new ChangeElementCommand(selected, initial)); - - return batchCmd; -}; - -// Root Current Transformation Matrix in user units -var root_sctm = null; - -// Group: Selection - -// Function: clearSelection -// Clears the selection. The 'selected' handler is then called. -// Parameters: -// noCall - Optional boolean that when true does not call the "selected" handler -var clearSelection = this.clearSelection = function(noCall) { - if (selectedElements[0] != null) { - var len = selectedElements.length; - for (var i = 0; i < len; ++i) { - var elem = selectedElements[i]; - if (elem == null) break; - selectorManager.releaseSelector(elem); - selectedElements[i] = null; - } -// selectedBBoxes[0] = null; - } - if(!noCall) call("selected", selectedElements); -}; - -// TODO: do we need to worry about selectedBBoxes here? - - -// Function: addToSelection -// Adds a list of elements to the selection. The 'selected' handler is then called. -// -// Parameters: -// elemsToAdd - an array of DOM elements to add to the selection -// showGrips - a boolean flag indicating whether the resize grips should be shown -var addToSelection = this.addToSelection = function(elemsToAdd, showGrips) { - if (elemsToAdd.length == 0) { return; } - // find the first null in our selectedElements array - var j = 0; - - while (j < selectedElements.length) { - if (selectedElements[j] == null) { - break; - } - ++j; - } - - // now add each element consecutively - var i = elemsToAdd.length; - while (i--) { - var elem = elemsToAdd[i]; - if (!elem || !svgedit.utilities.getBBox(elem)) continue; - - if(elem.tagName === 'a' && elem.childNodes.length === 1) { - // Make "a" element's child be the selected element - elem = elem.firstChild; - } - - // if it's not already there, add it - if (selectedElements.indexOf(elem) == -1) { - - selectedElements[j] = elem; - - // only the first selectedBBoxes element is ever used in the codebase these days -// if (j == 0) selectedBBoxes[0] = svgedit.utilities.getBBox(elem); - j++; - var sel = selectorManager.requestSelector(elem); - - if (selectedElements.length > 1) { - sel.showGrips(false); - } - } - } - call("selected", selectedElements); - - if (showGrips || selectedElements.length == 1) { - selectorManager.requestSelector(selectedElements[0]).showGrips(true); - } - else { - selectorManager.requestSelector(selectedElements[0]).showGrips(false); - } - - // make sure the elements are in the correct order - // See: http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition - - selectedElements.sort(function(a,b) { - if(a && b && a.compareDocumentPosition) { - return 3 - (b.compareDocumentPosition(a) & 6); - } else if(a == null) { - return 1; - } - }); - - // Make sure first elements are not null - while(selectedElements[0] == null) selectedElements.shift(0); -}; - -// Function: selectOnly() -// Selects only the given elements, shortcut for clearSelection(); addToSelection() -// -// Parameters: -// elems - an array of DOM elements to be selected -var selectOnly = this.selectOnly = function(elems, showGrips) { - clearSelection(true); - addToSelection(elems, showGrips); -} - -// TODO: could use slice here to make this faster? -// TODO: should the 'selected' handler - -// Function: removeFromSelection -// Removes elements from the selection. -// -// Parameters: -// elemsToRemove - an array of elements to remove from selection -var removeFromSelection = this.removeFromSelection = function(elemsToRemove) { - if (selectedElements[0] == null) { return; } - if (elemsToRemove.length == 0) { return; } - - // find every element and remove it from our array copy - var newSelectedItems = new Array(selectedElements.length); - j = 0, - len = selectedElements.length; - for (var i = 0; i < len; ++i) { - var elem = selectedElements[i]; - if (elem) { - // keep the item - if (elemsToRemove.indexOf(elem) == -1) { - newSelectedItems[j] = elem; - j++; - } - else { // remove the item and its selector - selectorManager.releaseSelector(elem); - } - } - } - // the copy becomes the master now - selectedElements = newSelectedItems; -}; - -// Function: selectAllInCurrentLayer -// Clears the selection, then adds all elements in the current layer to the selection. -this.selectAllInCurrentLayer = function() { - var current_layer = getCurrentDrawing().getCurrentLayer(); - if (current_layer) { - current_mode = "select"; - selectOnly($(current_group || current_layer).children()); - } -}; - -// Function: getMouseTarget -// Gets the desired element from a mouse event -// -// Parameters: -// evt - Event object from the mouse event -// -// Returns: -// DOM element we want -var getMouseTarget = this.getMouseTarget = function(evt) { - if (evt == null) { - return null; - } - var mouse_target = evt.target; - - // if it was a <use>, Opera and WebKit return the SVGElementInstance - if (mouse_target.correspondingUseElement) mouse_target = mouse_target.correspondingUseElement; - - // for foreign content, go up until we find the foreignObject - // WebKit browsers set the mouse target to the svgcanvas div - if ([mathns, htmlns].indexOf(mouse_target.namespaceURI) >= 0 && - mouse_target.id != "svgcanvas") - { - while (mouse_target.nodeName != "foreignObject") { - mouse_target = mouse_target.parentNode; - if(!mouse_target) return svgroot; - } - } - - // Get the desired mouse_target with jQuery selector-fu - // If it's root-like, select the root - var current_layer = getCurrentDrawing().getCurrentLayer(); - if([svgroot, container, svgcontent, current_layer].indexOf(mouse_target) >= 0) { - return svgroot; - } - - var $target = $(mouse_target); - - // If it's a selection grip, return the grip parent - if($target.closest('#selectorParentGroup').length) { - // While we could instead have just returned mouse_target, - // this makes it easier to indentify as being a selector grip - return selectorManager.selectorParentGroup; - } - - while (mouse_target.parentNode !== (current_group || current_layer)) { - mouse_target = mouse_target.parentNode; - } - -// -// // go up until we hit a child of a layer -// while (mouse_target.parentNode.parentNode.tagName == 'g') { -// mouse_target = mouse_target.parentNode; -// } - // Webkit bubbles the mouse event all the way up to the div, so we - // set the mouse_target to the svgroot like the other browsers -// if (mouse_target.nodeName.toLowerCase() == "div") { -// mouse_target = svgroot; -// } - - return mouse_target; -}; - -// Mouse events -(function() { - var d_attr = null, - start_x = null, - start_y = null, - r_start_x = null, - r_start_y = null, - init_bbox = {}, - freehand = { - minx: null, - miny: null, - maxx: null, - maxy: null - }; - - // - when we are in a create mode, the element is added to the canvas - // but the action is not recorded until mousing up - // - when we are in select mode, select the element, remember the position - // and do nothing else - var mouseDown = function(evt) - { - if(canvas.spaceKey || evt.button === 1) return; - - var right_click = evt.button === 2; - - if(evt.altKey) { // duplicate when dragging - svgCanvas.cloneSelectedElements(0,0); - } - - root_sctm = svgcontent.getScreenCTM().inverse(); - - var pt = transformPoint( evt.pageX, evt.pageY, root_sctm ), - mouse_x = pt.x * current_zoom, - mouse_y = pt.y * current_zoom; - - evt.preventDefault(); - - if(right_click) { - current_mode = "select"; - lastClickPoint = pt; - } - - // This would seem to be unnecessary... -// if(['select', 'resize'].indexOf(current_mode) == -1) { -// setGradient(); -// } - - var x = mouse_x / current_zoom, - y = mouse_y / current_zoom, - mouse_target = getMouseTarget(evt); - - if(mouse_target.tagName === 'a' && mouse_target.childNodes.length === 1) { - mouse_target = mouse_target.firstChild; - } - - // real_x/y ignores grid-snap value - var real_x = r_start_x = start_x = x; - var real_y = r_start_y = start_y = y; - - if(curConfig.gridSnapping){ - x = snapToGrid(x); - y = snapToGrid(y); - start_x = snapToGrid(start_x); - start_y = snapToGrid(start_y); - } - - // if it is a selector grip, then it must be a single element selected, - // set the mouse_target to that and update the mode to rotate/resize - - if (mouse_target == selectorManager.selectorParentGroup && selectedElements[0] != null) { - var grip = evt.target; - var griptype = elData(grip, "type"); - // rotating - if (griptype == "rotate") { - current_mode = "rotate"; - } - // resizing - else if(griptype == "resize") { - current_mode = "resize"; - current_resize_mode = elData(grip, "dir"); - } - mouse_target = selectedElements[0]; - } - - start_transform = mouse_target.getAttribute("transform"); - var tlist = getTransformList(mouse_target); - // handle undesired attribute changes when switching from one tool to another - if(typeof last_mode == 'undefined') { - last_mode = ""; - } - if(typeof line_stroke == 'undefined') { - line_stroke = "#" + curConfig.initStroke.color; - } - if (last_mode == "text") { - // using the text tool would normally change the fill and stroke - cur_shape.fill = "#" + curConfig.initFill.color - cur_shape.stroke = "#2D2D2D" - last_mode = "" - } else if (last_mode == "fhpath") { - // using the pencil tool would normally change the fill - cur_shape.fill = "#" + curConfig.initFill.color - last_mode = "" - } else if (last_mode == "line" && current_mode == "line") { - line_stroke = cur_shape.stroke - }; - switch (current_mode) { - case "select": - started = true; - current_resize_mode = "none"; - if(right_click) started = false; - - if (mouse_target != svgroot) { - // if this element is not yet selected, clear selection and select it - if (selectedElements.indexOf(mouse_target) == -1) { - // only clear selection if shift is not pressed (otherwise, add - // element to selection) - if (!evt.shiftKey) { - // No need to do the call here as it will be done on addToSelection - clearSelection(true); - } - addToSelection([mouse_target]); - justSelected = mouse_target; - pathActions.clear(); - } - // else if it's a path, go into pathedit mode in mouseup - - if(!right_click) { - // insert a dummy transform so if the element(s) are moved it will have - // a transform to use for its translate - for (var i = 0; i < selectedElements.length; ++i) { - if(selectedElements[i] == null) continue; - var slist = getTransformList(selectedElements[i]); - if(slist.numberOfItems) { - slist.insertItemBefore(svgroot.createSVGTransform(), 0); - } else { - slist.appendItem(svgroot.createSVGTransform()); - } - } - } - } - else if(!right_click){ - clearSelection(); - current_mode = "multiselect"; - if (rubberBox == null) { - rubberBox = selectorManager.getRubberBandBox(); - } - r_start_x *= current_zoom; - r_start_y *= current_zoom; -// console.log('p',[evt.pageX, evt.pageY]); -// console.log('c',[evt.clientX, evt.clientY]); -// console.log('o',[evt.offsetX, evt.offsetY]); -// console.log('s',[start_x, start_y]); - - assignAttributes(rubberBox, { - 'x': r_start_x, - 'y': r_start_y, - 'width': 0, - 'height': 0, - 'display': 'inline' - }, 100); - } - break; - case "zoom": - started = true; - if (rubberBox == null) { - rubberBox = selectorManager.getRubberBandBox(); - } - assignAttributes(rubberBox, { - 'x': real_x * current_zoom, - 'y': real_x * current_zoom, - 'width': 0, - 'height': 0, - 'display': 'inline' - }, 100); - break; - case "resize": - started = true; - start_x = x; - start_y = y; - - // Getting the BBox from the selection box, since we know we - // want to orient around it - init_bbox = svgedit.utilities.getBBox($('#selectedBox0')[0]); - var bb = {}; - $.each(init_bbox, function(key, val) { - bb[key] = val/current_zoom; - }); - init_bbox = bb; - - // append three dummy transforms to the tlist so that - // we can translate,scale,translate in mousemove - var pos = getRotationAngle(mouse_target)?1:0; - - if(hasMatrixTransform(tlist)) { - tlist.insertItemBefore(svgroot.createSVGTransform(), pos); - tlist.insertItemBefore(svgroot.createSVGTransform(), pos); - tlist.insertItemBefore(svgroot.createSVGTransform(), pos); - } else { - tlist.appendItem(svgroot.createSVGTransform()); - tlist.appendItem(svgroot.createSVGTransform()); - tlist.appendItem(svgroot.createSVGTransform()); - - if(svgedit.browser.supportsNonScalingStroke()) { - // Handle crash for newer Chrome and Safari 6 (Mobile and Desktop): - // https://code.google.com/p/svg-edit/issues/detail?id=904 - // Chromium issue: https://code.google.com/p/chromium/issues/detail?id=114625 - // TODO: Remove this workaround once vendor fixes the issue - var isWebkit = svgedit.browser.isWebkit(); - - if(isWebkit) { - var delayedStroke = function(ele) { - var _stroke = ele.getAttributeNS(null, 'stroke'); - ele.removeAttributeNS(null, 'stroke'); - //Re-apply stroke after delay. Anything higher than 1 seems to cause flicker - setTimeout(function() { ele.setAttributeNS(null, 'stroke', _stroke) }, 0); - } - } - mouse_target.style.vectorEffect = 'non-scaling-stroke'; - if(isWebkit) delayedStroke(mouse_target); - - var all = mouse_target.getElementsByTagName('*'), - len = all.length; - for(var i = 0; i < len; i++) { - all[i].style.vectorEffect = 'non-scaling-stroke'; - if(isWebkit) delayedStroke(all[i]); - } - } - } - break; - case "fhellipse": - case "fhrect": - case "fhpath": - started = true; - last_mode = "fhpath"; - d_attr = real_x + "," + real_y + " "; - var stroke_w = cur_shape.stroke_width == 0?1:cur_shape.stroke_width; - addSvgElementFromJson({ - "element": "polyline", - "curStyles": true, - "attr": { - "points": d_attr, - "id": getNextId(), - "fill": "none", - "opacity": cur_shape.opacity, - "stroke-linecap": "round", - "style": "pointer-events:none" - } - }); - freehand.minx = real_x; - freehand.maxx = real_x; - freehand.miny = real_y; - freehand.maxy = real_y; - break; - case "image": - started = true; - last_mode = "image" - var newImage = addSvgElementFromJson({ - "element": "image", - "attr": { - "x": x, - "y": y, - "width": 0, - "height": 0, - "id": getNextId(), - "opacity": cur_shape.opacity, - "style": "pointer-events:inherit" - } - }); - setHref(newImage, last_good_img_url); - preventClickDefault(newImage); - break; - case "square": - // FIXME: once we create the rect, we lose information that this was a square - // (for resizing purposes this could be important) - case "rect": - started = true; - last_mode = "rect"; - start_x = x; - start_y = y; - addSvgElementFromJson({ - "element": "rect", - "curStyles": true, - "attr": { - "x": x, - "y": y, - "width": 0, - "height": 0, - "id": getNextId(), - "opacity": cur_shape.opacity, - "stroke": "#2D2D2D", - "stroke-width": 1, - "stroke-opacity": 1 - } - }); - break; - case "line": - started = true; - last_mode = "line" - // use current stroke width. if width 0, use initial width - var stroke_w = cur_shape.stroke_width == 0?curConfig.initStroke.width:cur_shape.stroke_width; - addSvgElementFromJson({ - "element": "line", - "curStyles": true, - "attr": { - "x1": x, - "y1": y, - "x2": x, - "y2": y, - "id": getNextId(), - "stroke": line_stroke, - "stroke-width": stroke_w, - "stroke-dasharray": cur_shape.stroke_dasharray, - "stroke-linejoin": cur_shape.stroke_linejoin, - "stroke-linecap": cur_shape.stroke_linecap, - "stroke-opacity": cur_shape.stroke_opacity, - "fill": cur_shape.fill, - "opacity": cur_shape.opacity, - "style": "pointer-events:none" - } - }); - break; - case "circle": - started = true; - last_mode = "circle" - addSvgElementFromJson({ - "element": "circle", - "curStyles": true, - "attr": { - "cx": x, - "cy": y, - "r": 0, - "id": getNextId(), - "opacity": cur_shape.opacity, - "stroke": "#2D2D2D", - "stroke-width": 1, - "stroke-opacity": 1 - } - }); - break; - case "ellipse": - started = true; - last_mode = "elipse" - addSvgElementFromJson({ - "element": "ellipse", - "curStyles": true, - "attr": { - "cx": x, - "cy": y, - "rx": 0, - "ry": 0, - "id": getNextId(), - "opacity": cur_shape.opacity, - "stroke": "#2D2D2D", - "stroke-width": 1, - "stroke-opacity": 1 - } - }); - break; - case "text": - started = true; - last_mode = "text"; - var newText = addSvgElementFromJson({ - "element": "text", - "curStyles": true, - "attr": { - "x": x, - "y": y, - "id": getNextId(), - "fill": cur_text.fill, - "stroke": "none", - "stroke-width": 0, - "font-size": cur_text.font_size, - "font-family": cur_text.font_family, - "text-anchor": "middle", - "xml:space": "preserve", - "opacity": cur_shape.opacity - } - }); -// newText.textContent = "text"; - break; - case "path": - // Fall through - case "pathedit": - start_x *= current_zoom; - start_y *= current_zoom; - pathActions.mouseDown(evt, mouse_target, start_x, start_y); - started = true; - break; - case "textedit": - last_mode = "text" - start_x *= current_zoom; - start_y *= current_zoom; - textActions.mouseDown(evt, mouse_target, start_x, start_y); - started = true; - break; - case "rotate": - started = true; - // we are starting an undoable change (a drag-rotation) - canvas.undoMgr.beginUndoableChange("transform", selectedElements); - break; - default: - // This could occur in an extension - break; - } - - var ext_result = runExtensions("mouseDown", { - event: evt, - start_x: start_x, - start_y: start_y, - selectedElements: selectedElements - }, true); - - $.each(ext_result, function(i, r) { - if(r && r.started) { - started = true; - } - }); - }; - - // in this function we do not record any state changes yet (but we do update - // any elements that are still being created, moved or resized on the canvas) - var mouseMove = function(evt) - { - if (!started) return; - if(evt.button === 1 || canvas.spaceKey) return; - - var selected = selectedElements[0], - pt = transformPoint( evt.pageX, evt.pageY, root_sctm ), - mouse_x = pt.x * current_zoom, - mouse_y = pt.y * current_zoom, - shape = getElem(getId()); - - var real_x = x = mouse_x / current_zoom; - var real_y = y = mouse_y / current_zoom; - - if(curConfig.gridSnapping){ - x = snapToGrid(x); - y = snapToGrid(y); - } - - evt.preventDefault(); - - switch (current_mode) - { - case "select": - // we temporarily use a translate on the element(s) being dragged - // this transform is removed upon mousing up and the element is - // relocated to the new location - if (selectedElements[0] !== null) { - var dx = x - start_x; - var dy = y - start_y; - - if(curConfig.gridSnapping){ - dx = snapToGrid(dx); - dy = snapToGrid(dy); - } - - if(evt.shiftKey) { var xya = snapToAngle(start_x,start_y,x,y); x=xya.x; y=xya.y; } - - if (dx != 0 || dy != 0) { - var len = selectedElements.length; - for (var i = 0; i < len; ++i) { - var selected = selectedElements[i]; - if (selected == null) break; -// if (i==0) { -// var box = svgedit.utilities.getBBox(selected); -// selectedBBoxes[i].x = box.x + dx; -// selectedBBoxes[i].y = box.y + dy; -// } - - // update the dummy transform in our transform list - // to be a translate - var xform = svgroot.createSVGTransform(); - var tlist = getTransformList(selected); - // Note that if Webkit and there's no ID for this - // element, the dummy transform may have gotten lost. - // This results in unexpected behaviour - - xform.setTranslate(dx,dy); - if(tlist.numberOfItems) { - tlist.replaceItem(xform, 0); - } else { - tlist.appendItem(xform); - } - - // update our internal bbox that we're tracking while dragging - selectorManager.requestSelector(selected).resize(); - } - - call("transition", selectedElements); - } - } - break; - case "multiselect": - real_x *= current_zoom; - real_y *= current_zoom; - assignAttributes(rubberBox, { - 'x': Math.min(r_start_x, real_x), - 'y': Math.min(r_start_y, real_y), - 'width': Math.abs(real_x - r_start_x), - 'height': Math.abs(real_y - r_start_y) - },100); - - // for each selected: - // - if newList contains selected, do nothing - // - if newList doesn't contain selected, remove it from selected - // - for any newList that was not in selectedElements, add it to selected - var elemsToRemove = [], elemsToAdd = [], - newList = getIntersectionList(), - len = selectedElements.length; - - for (var i = 0; i < len; ++i) { - var ind = newList.indexOf(selectedElements[i]); - if (ind == -1) { - elemsToRemove.push(selectedElements[i]); - } - else { - newList[ind] = null; - } - } - - len = newList.length; - for (i = 0; i < len; ++i) { if (newList[i]) elemsToAdd.push(newList[i]); } - - if (elemsToRemove.length > 0) - canvas.removeFromSelection(elemsToRemove); - - if (elemsToAdd.length > 0) - addToSelection(elemsToAdd); - - break; - case "resize": - // we track the resize bounding box and translate/scale the selected element - // while the mouse is down, when mouse goes up, we use this to recalculate - // the shape's coordinates - var tlist = getTransformList(selected), - hasMatrix = hasMatrixTransform(tlist), - box = hasMatrix ? init_bbox : svgedit.utilities.getBBox(selected), - left=box.x, top=box.y, width=box.width, - height=box.height, dx=(x-start_x), dy=(y-start_y); - - if(curConfig.gridSnapping){ - dx = snapToGrid(dx); - dy = snapToGrid(dy); - height = snapToGrid(height); - width = snapToGrid(width); - } - - // if rotated, adjust the dx,dy values - var angle = getRotationAngle(selected); - if (angle) { - var r = Math.sqrt( dx*dx + dy*dy ), - theta = Math.atan2(dy,dx) - angle * Math.PI / 180.0; - dx = r * Math.cos(theta); - dy = r * Math.sin(theta); - } - - // if not stretching in y direction, set dy to 0 - // if not stretching in x direction, set dx to 0 - if(current_resize_mode.indexOf("n")==-1 && current_resize_mode.indexOf("s")==-1) { - dy = 0; - } - if(current_resize_mode.indexOf("e")==-1 && current_resize_mode.indexOf("w")==-1) { - dx = 0; - } - - var ts = null, - tx = 0, ty = 0, - sy = height ? (height+dy)/height : 1, - sx = width ? (width+dx)/width : 1; - // if we are dragging on the north side, then adjust the scale factor and ty - if(current_resize_mode.indexOf("n") >= 0) { - sy = height ? (height-dy)/height : 1; - ty = height; - } - - // if we dragging on the east side, then adjust the scale factor and tx - if(current_resize_mode.indexOf("w") >= 0) { - sx = width ? (width-dx)/width : 1; - tx = width; - } - - // update the transform list with translate,scale,translate - var translateOrigin = svgroot.createSVGTransform(), - scale = svgroot.createSVGTransform(), - translateBack = svgroot.createSVGTransform(); - - if(curConfig.gridSnapping){ - left = snapToGrid(left); - tx = snapToGrid(tx); - top = snapToGrid(top); - ty = snapToGrid(ty); - } - - translateOrigin.setTranslate(-(left+tx),-(top+ty)); - if(evt.shiftKey) { - if(sx == 1) sx = sy - else sy = sx; - } - scale.setScale(sx,sy); - - translateBack.setTranslate(left+tx,top+ty); - if(hasMatrix) { - var diff = angle?1:0; - tlist.replaceItem(translateOrigin, 2+diff); - tlist.replaceItem(scale, 1+diff); - tlist.replaceItem(translateBack, 0+diff); - } else { - var N = tlist.numberOfItems; - tlist.replaceItem(translateBack, N-3); - tlist.replaceItem(scale, N-2); - tlist.replaceItem(translateOrigin, N-1); - } - - selectorManager.requestSelector(selected).resize(); - - call("transition", selectedElements); - - break; - case "zoom": - real_x *= current_zoom; - real_y *= current_zoom; - assignAttributes(rubberBox, { - 'x': Math.min(r_start_x*current_zoom, real_x), - 'y': Math.min(r_start_y*current_zoom, real_y), - 'width': Math.abs(real_x - r_start_x*current_zoom), - 'height': Math.abs(real_y - r_start_y*current_zoom) - },100); - break; - case "text": - assignAttributes(shape,{ - 'x': x, - 'y': y - },1000); - break; - case "line": - // Opera has a problem with suspendRedraw() apparently - var handle = null; - if (!window.opera) svgroot.suspendRedraw(1000); - - if(curConfig.gridSnapping){ - x = snapToGrid(x); - y = snapToGrid(y); - } - - var x2 = x; - var y2 = y; - - if(evt.shiftKey) { var xya = snapToAngle(start_x,start_y,x2,y2); x2=xya.x; y2=xya.y; } - - shape.setAttributeNS(null, "x2", x2); - shape.setAttributeNS(null, "y2", y2); - if (!window.opera) svgroot.unsuspendRedraw(handle); - break; - case "foreignObject": - // fall through - case "square": - // fall through - case "rect": - // fall through - case "image": - var square = (current_mode == 'square') || evt.shiftKey, - w = Math.abs(x - start_x), - h = Math.abs(y - start_y), - new_x, new_y; - if(square) { - w = h = Math.max(w, h); - new_x = start_x < x ? start_x : start_x - w; - new_y = start_y < y ? start_y : start_y - h; - } else { - new_x = Math.min(start_x,x); - new_y = Math.min(start_y,y); - } - - if(curConfig.gridSnapping){ - w = snapToGrid(w); - h = snapToGrid(h); - new_x = snapToGrid(new_x); - new_y = snapToGrid(new_y); - } - - assignAttributes(shape,{ - 'width': w, - 'height': h, - 'x': new_x, - 'y': new_y - },1000); - - break; - case "circle": - var c = $(shape).attr(["cx", "cy"]); - var cx = c.cx, cy = c.cy, - rad = Math.sqrt( (x-cx)*(x-cx) + (y-cy)*(y-cy) ); - if(curConfig.gridSnapping){ - rad = snapToGrid(rad); - } - shape.setAttributeNS(null, "r", rad); - break; - case "ellipse": - var c = $(shape).attr(["cx", "cy"]); - var cx = c.cx, cy = c.cy; - // Opera has a problem with suspendRedraw() apparently - handle = null; - if (!window.opera) svgroot.suspendRedraw(1000); - if(curConfig.gridSnapping){ - x = snapToGrid(x); - cx = snapToGrid(cx); - y = snapToGrid(y); - cy = snapToGrid(cy); - } - shape.setAttributeNS(null, "rx", Math.abs(x - cx) ); - var ry = Math.abs(evt.shiftKey?(x - cx):(y - cy)); - shape.setAttributeNS(null, "ry", ry ); - if (!window.opera) svgroot.unsuspendRedraw(handle); - break; - case "fhellipse": - case "fhrect": - freehand.minx = Math.min(real_x, freehand.minx); - freehand.maxx = Math.max(real_x, freehand.maxx); - freehand.miny = Math.min(real_y, freehand.miny); - freehand.maxy = Math.max(real_y, freehand.maxy); - // break; missing on purpose - case "fhpath": - d_attr += + real_x + "," + real_y + " "; - shape.setAttributeNS(null, "points", d_attr); - break; - // update path stretch line coordinates - case "path": - // fall through - case "pathedit": - x *= current_zoom; - y *= current_zoom; - - if(curConfig.gridSnapping){ - x = snapToGrid(x); - y = snapToGrid(y); - start_x = snapToGrid(start_x); - start_y = snapToGrid(start_y); - } - if(evt.shiftKey) { - var path = svgedit.path.path; - if(path) { - var x1 = path.dragging?path.dragging[0]:start_x; - var y1 = path.dragging?path.dragging[1]:start_y; - } else { - var x1 = start_x; - var y1 = start_y; - } - var xya = snapToAngle(x1,y1,x,y); - x=xya.x; y=xya.y; - } - - if(rubberBox && rubberBox.getAttribute('display') !== 'none') { - real_x *= current_zoom; - real_y *= current_zoom; - assignAttributes(rubberBox, { - 'x': Math.min(r_start_x*current_zoom, real_x), - 'y': Math.min(r_start_y*current_zoom, real_y), - 'width': Math.abs(real_x - r_start_x*current_zoom), - 'height': Math.abs(real_y - r_start_y*current_zoom) - },100); - } - pathActions.mouseMove(x, y); - - break; - case "textedit": - x *= current_zoom; - y *= current_zoom; -// if(rubberBox && rubberBox.getAttribute('display') != 'none') { -// assignAttributes(rubberBox, { -// 'x': Math.min(start_x,x), -// 'y': Math.min(start_y,y), -// 'width': Math.abs(x-start_x), -// 'height': Math.abs(y-start_y) -// },100); -// } - - textActions.mouseMove(mouse_x, mouse_y); - - break; - case "rotate": - var box = svgedit.utilities.getBBox(selected), - cx = box.x + box.width/2, - cy = box.y + box.height/2, - m = getMatrix(selected), - center = transformPoint(cx,cy,m); - cx = center.x; - cy = center.y; - var angle = ((Math.atan2(cy-y,cx-x) * (180/Math.PI))-90) % 360; - if(curConfig.gridSnapping){ - angle = snapToGrid(angle); - } - if(evt.shiftKey) { // restrict rotations to nice angles (WRS) - var snap = 45; - angle= Math.round(angle/snap)*snap; - } - - canvas.setRotationAngle(angle<-180?(360+angle):angle, true); - call("transition", selectedElements); - break; - default: - break; - } - - runExtensions("mouseMove", { - event: evt, - mouse_x: mouse_x, - mouse_y: mouse_y, - selected: selected - }); - - }; // mouseMove() - - // - in create mode, the element's opacity is set properly, we create an InsertElementCommand - // and store it on the Undo stack - // - in move/resize mode, the element's attributes which were affected by the move/resize are - // identified, a ChangeElementCommand is created and stored on the stack for those attrs - // this is done in when we recalculate the selected dimensions() - var mouseUp = function(evt) - { - if(evt.button === 2) return; - var tempJustSelected = justSelected; - justSelected = null; - if (!started) return; - var pt = transformPoint( evt.pageX, evt.pageY, root_sctm ), - mouse_x = pt.x * current_zoom, - mouse_y = pt.y * current_zoom, - x = mouse_x / current_zoom, - y = mouse_y / current_zoom, - element = getElem(getId()), - keep = false; - - var real_x = x; - var real_y = y; - - // TODO: Make true when in multi-unit mode - var useUnit = false; // (curConfig.baseUnit !== 'px'); - started = false; - switch (current_mode) - { - // intentionally fall-through to select here - case "resize": - case "multiselect": - if (rubberBox != null) { - rubberBox.setAttribute("display", "none"); - curBBoxes = []; - } - current_mode = "select"; - case "select": - if (selectedElements[0] != null) { - // if we only have one selected element - if (selectedElements[1] == null) { - // set our current stroke/fill properties to the element's - var selected = selectedElements[0]; - switch ( selected.tagName ) { - case "g": - case "use": - case "image": - case "foreignObject": - break; - default: - cur_properties.fill = selected.getAttribute("fill"); - cur_properties.fill_opacity = selected.getAttribute("fill-opacity"); - cur_properties.stroke = selected.getAttribute("stroke"); - cur_properties.stroke_opacity = selected.getAttribute("stroke-opacity"); - cur_properties.stroke_width = selected.getAttribute("stroke-width"); - cur_properties.stroke_dasharray = selected.getAttribute("stroke-dasharray"); - cur_properties.stroke_linejoin = selected.getAttribute("stroke-linejoin"); - cur_properties.stroke_linecap = selected.getAttribute("stroke-linecap"); - } - - if (selected.tagName == "text") { - cur_text.font_size = selected.getAttribute("font-size"); - cur_text.font_family = selected.getAttribute("font-family"); - } - selectorManager.requestSelector(selected).showGrips(true); - - // This shouldn't be necessary as it was done on mouseDown... -// call("selected", [selected]); - } - // always recalculate dimensions to strip off stray identity transforms - recalculateAllSelectedDimensions(); - // if it was being dragged/resized - if (real_x != r_start_x || real_y != r_start_y) { - var len = selectedElements.length; - for (var i = 0; i < len; ++i) { - if (selectedElements[i] == null) break; - if(!selectedElements[i].firstChild) { - // Not needed for groups (incorrectly resizes elems), possibly not needed at all? - selectorManager.requestSelector(selectedElements[i]).resize(); - } - } - } - // no change in position/size, so maybe we should move to pathedit - else { - var t = evt.target; - if (selectedElements[0].nodeName === "path" && selectedElements[1] == null) { - pathActions.select(selectedElements[0]); - } // if it was a path - // else, if it was selected and this is a shift-click, remove it from selection - else if (evt.shiftKey) { - if(tempJustSelected != t) { - canvas.removeFromSelection([t]); - } - } - } // no change in mouse position - - // Remove non-scaling stroke - if(svgedit.browser.supportsNonScalingStroke()) { - var elem = selectedElements[0]; - if (elem) { - elem.removeAttribute('style'); - svgedit.utilities.walkTree(elem, function(elem) { - elem.removeAttribute('style'); - }); - } - } - - } - return; - break; - case "zoom": - if (rubberBox != null) { - rubberBox.setAttribute("display", "none"); - } - var factor = evt.shiftKey?.5:2; - call("zoomed", { - 'x': Math.min(r_start_x, real_x), - 'y': Math.min(r_start_y, real_y), - 'width': Math.abs(real_x - r_start_x), - 'height': Math.abs(real_y - r_start_y), - 'factor': factor - }); - return; - case "fhpath": - // Check that the path contains at least 2 points; a degenerate one-point path - // causes problems. - // Webkit ignores how we set the points attribute with commas and uses space - // to separate all coordinates, see https://bugs.webkit.org/show_bug.cgi?id=29870 - var coords = element.getAttribute('points'); - var commaIndex = coords.indexOf(','); - if (commaIndex >= 0) { - keep = coords.indexOf(',', commaIndex+1) >= 0; - } else { - keep = coords.indexOf(' ', coords.indexOf(' ')+1) >= 0; - } - if (keep) { - element = pathActions.smoothPolylineIntoPath(element); - } - break; - case "line": - var attrs = $(element).attr(["x1", "x2", "y1", "y2"]); - keep = (attrs.x1 != attrs.x2 || attrs.y1 != attrs.y2); - break; - case "foreignObject": - case "square": - case "rect": - case "image": - var attrs = $(element).attr(["width", "height"]); - // Image should be kept regardless of size (use inherit dimensions later) - keep = (attrs.width != 0 || attrs.height != 0) || current_mode === "image"; - break; - case "circle": - keep = (element.getAttribute('r') != 0); - break; - case "ellipse": - var attrs = $(element).attr(["rx", "ry"]); - keep = (attrs.rx != null || attrs.ry != null); - break; - case "fhellipse": - if ((freehand.maxx - freehand.minx) > 0 && - (freehand.maxy - freehand.miny) > 0) { - element = addSvgElementFromJson({ - "element": "ellipse", - "curStyles": true, - "attr": { - "cx": (freehand.minx + freehand.maxx) / 2, - "cy": (freehand.miny + freehand.maxy) / 2, - "rx": (freehand.maxx - freehand.minx) / 2, - "ry": (freehand.maxy - freehand.miny) / 2, - "id": getId() - } - }); - call("changed",[element]); - keep = true; - } - break; - case "fhrect": - if ((freehand.maxx - freehand.minx) > 0 && - (freehand.maxy - freehand.miny) > 0) { - element = addSvgElementFromJson({ - "element": "rect", - "curStyles": true, - "attr": { - "x": freehand.minx, - "y": freehand.miny, - "width": (freehand.maxx - freehand.minx), - "height": (freehand.maxy - freehand.miny), - "id": getId() - } - }); - call("changed",[element]); - keep = true; - } - break; - case "text": - keep = true; - selectOnly([element]); - textActions.start(element); - break; - case "path": - // set element to null here so that it is not removed nor finalized - element = null; - // continue to be set to true so that mouseMove happens - started = true; - - var res = pathActions.mouseUp(evt, element, mouse_x, mouse_y); - element = res.element - keep = res.keep; - break; - case "pathedit": - keep = true; - element = null; - pathActions.mouseUp(evt); - break; - case "textedit": - keep = false; - element = null; - textActions.mouseUp(evt, mouse_x, mouse_y); - break; - case "rotate": - keep = true; - element = null; - current_mode = "select"; - var batchCmd = canvas.undoMgr.finishUndoableChange(); - if (!batchCmd.isEmpty()) { - addCommandToHistory(batchCmd); - } - // perform recalculation to weed out any stray identity transforms that might get stuck - recalculateAllSelectedDimensions(); - call("changed", selectedElements); - break; - default: - // This could occur in an extension - break; - } - - var ext_result = runExtensions("mouseUp", { - event: evt, - mouse_x: mouse_x, - mouse_y: mouse_y - }, true); - - $.each(ext_result, function(i, r) { - if(r) { - keep = r.keep || keep; - element = r.element; - started = r.started || started; - } - }); - - if (!keep && element != null) { - getCurrentDrawing().releaseId(getId()); - element.parentNode.removeChild(element); - element = null; - - var t = evt.target; - - // if this element is in a group, go up until we reach the top-level group - // just below the layer groups - // TODO: once we implement links, we also would have to check for <a> elements - while (t.parentNode.parentNode.tagName == "g") { - t = t.parentNode; - } - // if we are not in the middle of creating a path, and we've clicked on some shape, - // then go to Select mode. - // WebKit returns <div> when the canvas is clicked, Firefox/Opera return <svg> - if ( (current_mode != "path" || !drawn_path) && - t.parentNode.id != "selectorParentGroup" && - t.id != "svgcanvas" && t.id != "svgroot") - { - // switch into "select" mode if we've clicked on an element - canvas.setMode("select"); - selectOnly([t], true); - } - - } else if (element != null) { - canvas.addedNew = true; - - if(useUnit) svgedit.units.convertAttrs(element); - - var ani_dur = .2, c_ani; - if(opac_ani.beginElement && element.getAttribute('opacity') != cur_shape.opacity) { - c_ani = $(opac_ani).clone().attr({ - to: cur_shape.opacity, - dur: ani_dur - }).appendTo(element); - try { - // Fails in FF4 on foreignObject - c_ani[0].beginElement(); - } catch(e){} - } else { - ani_dur = 0; - } - - // Ideally this would be done on the endEvent of the animation, - // but that doesn't seem to be supported in Webkit - setTimeout(function() { - if(c_ani) c_ani.remove(); - element.setAttribute("opacity", cur_shape.opacity); - element.setAttribute("style", "pointer-events:inherit"); - cleanupElement(element); - if(current_mode === "path") { - pathActions.toEditMode(element); - } else { - if(curConfig.selectNew) { - selectOnly([element], true); - } - } - // we create the insert command that is stored on the stack - // undo means to call cmd.unapply(), redo means to call cmd.apply() - addCommandToHistory(new InsertElementCommand(element)); - - call("changed",[element]); - }, ani_dur * 1000); - } - - start_transform = null; - }; - - var dblClick = function(evt) { - var evt_target = evt.target; - var parent = evt_target.parentNode; - - // Do nothing if already in current group - if(parent === current_group) return; - - var mouse_target = getMouseTarget(evt); - var tagName = mouse_target.tagName; - - if(tagName === 'text' && current_mode !== 'textedit') { - var pt = transformPoint( evt.pageX, evt.pageY, root_sctm ); - textActions.select(mouse_target, pt.x, pt.y); - } - - if((tagName === "g" || tagName === "a") && getRotationAngle(mouse_target)) { - // TODO: Allow method of in-group editing without having to do - // this (similar to editing rotated paths) - - // Ungroup and regroup - pushGroupProperties(mouse_target); - mouse_target = selectedElements[0]; - clearSelection(true); - } - // Reset context - if(current_group) { - leaveContext(); - } - - if((parent.tagName !== 'g' && parent.tagName !== 'a') || - parent === getCurrentDrawing().getCurrentLayer() || - mouse_target === selectorManager.selectorParentGroup) - { - // Escape from in-group edit - return; - } - setContext(mouse_target); - } - - // prevent links from being followed in the canvas - var handleLinkInCanvas = function(e) { - e.preventDefault(); - return false; - }; - - // Added mouseup to the container here. - // TODO(codedread): Figure out why after the Closure compiler, the window mouseup is ignored. - $(container).mousedown(mouseDown).mousemove(mouseMove).click(handleLinkInCanvas).dblclick(dblClick).mouseup(mouseUp); - // $(window).mouseup(mouseUp); - - //TODO(rafaelcastrocouto): User preference for shift key and zoom factor - $(container).bind("mousewheel DOMMouseScroll", function(e){ - - if (!e.ctrlKey) {return;} - e.preventDefault(); - var evt = e.originalEvent; - - - root_sctm = $('#svgcontent g')[0].getScreenCTM().inverse(); - var pt = svgedit.math.transformPoint( evt.pageX, evt.pageY, root_sctm ); - - var bbox = { - 'x': pt.x, - 'y': pt.y, - 'width': 0, - 'height': 0 - }; - - var delta = (evt.wheelDelta) ? evt.wheelDelta : (evt.detail) ? -evt.detail : 0; - if (!delta) {return;} - - bbox.factor = Math.max(3/4, Math.min(4/3, (delta))); - - call("zoomed", bbox); - }); - -}()); - -// Function: preventClickDefault -// Prevents default browser click behaviour on the given element -// -// Parameters: -// img - The DOM element to prevent the cilck on -var preventClickDefault = function(img) { - $(img).click(function(e){e.preventDefault()}); -} - -// Group: Text edit functions -// Functions relating to editing text elements -var textActions = canvas.textActions = function() { - var curtext; - var textinput; - var cursor; - var selblock; - var blinker; - var chardata = []; - var textbb, transbb; - var matrix; - var last_x, last_y; - var allow_dbl; - - function setCursor(index) { - var empty = (textinput.value === ""); - $(textinput).focus(); - - if(!arguments.length) { - if(empty) { - index = 0; - } else { - if(textinput.selectionEnd !== textinput.selectionStart) return; - index = textinput.selectionEnd; - } - } - - var charbb; - charbb = chardata[index]; - if(!empty) { - textinput.setSelectionRange(index, index); - } - cursor = getElem("text_cursor"); - if (!cursor) { - cursor = document.createElementNS(svgns, "line"); - assignAttributes(cursor, { - 'id': "text_cursor", - 'stroke': "#333", - 'stroke-width': 1 - }); - cursor = getElem("selectorParentGroup").appendChild(cursor); - } - - if(!blinker) { - blinker = setInterval(function() { - var show = (cursor.getAttribute('display') === 'none'); - cursor.setAttribute('display', show?'inline':'none'); - }, 600); - - } - - - var start_pt = ptToScreen(charbb.x, textbb.y); - var end_pt = ptToScreen(charbb.x, (textbb.y + textbb.height)); - - assignAttributes(cursor, { - x1: start_pt.x, - y1: start_pt.y, - x2: end_pt.x, - y2: end_pt.y, - visibility: 'visible', - display: 'inline' - }); - - if(selblock) selblock.setAttribute('d', ''); - } - - function setSelection(start, end, skipInput) { - if(start === end) { - setCursor(end); - return; - } - - if(!skipInput) { - textinput.setSelectionRange(start, end); - } - - selblock = getElem("text_selectblock"); - if (!selblock) { - - selblock = document.createElementNS(svgns, "path"); - assignAttributes(selblock, { - 'id': "text_selectblock", - 'fill': "green", - 'opacity': .5, - 'style': "pointer-events:none" - }); - getElem("selectorParentGroup").appendChild(selblock); - } - - - var startbb = chardata[start]; - - var endbb = chardata[end]; - - cursor.setAttribute('visibility', 'hidden'); - - var tl = ptToScreen(startbb.x, textbb.y), - tr = ptToScreen(startbb.x + (endbb.x - startbb.x), textbb.y), - bl = ptToScreen(startbb.x, textbb.y + textbb.height), - br = ptToScreen(startbb.x + (endbb.x - startbb.x), textbb.y + textbb.height); - - - var dstr = "M" + tl.x + "," + tl.y - + " L" + tr.x + "," + tr.y - + " " + br.x + "," + br.y - + " " + bl.x + "," + bl.y + "z"; - - assignAttributes(selblock, { - d: dstr, - 'display': 'inline' - }); - } - - function getIndexFromPoint(mouse_x, mouse_y) { - // Position cursor here - var pt = svgroot.createSVGPoint(); - pt.x = mouse_x; - pt.y = mouse_y; - - // No content, so return 0 - if(chardata.length == 1) return 0; - // Determine if cursor should be on left or right of character - var charpos = curtext.getCharNumAtPosition(pt); - if(charpos < 0) { - // Out of text range, look at mouse coords - charpos = chardata.length - 2; - if(mouse_x <= chardata[0].x) { - charpos = 0; - } - } else if(charpos >= chardata.length - 2) { - charpos = chardata.length - 2; - } - var charbb = chardata[charpos]; - var mid = charbb.x + (charbb.width/2); - if(mouse_x > mid) { - charpos++; - } - return charpos; - } - - function setCursorFromPoint(mouse_x, mouse_y) { - setCursor(getIndexFromPoint(mouse_x, mouse_y)); - } - - function setEndSelectionFromPoint(x, y, apply) { - var i1 = textinput.selectionStart; - var i2 = getIndexFromPoint(x, y); - - var start = Math.min(i1, i2); - var end = Math.max(i1, i2); - setSelection(start, end, !apply); - } - - function screenToPt(x_in, y_in) { - var out = { - x: x_in, - y: y_in - } - - out.x /= current_zoom; - out.y /= current_zoom; - - if(matrix) { - var pt = transformPoint(out.x, out.y, matrix.inverse()); - out.x = pt.x; - out.y = pt.y; - } - - return out; - } - - function ptToScreen(x_in, y_in) { - var out = { - x: x_in, - y: y_in - } - - if(matrix) { - var pt = transformPoint(out.x, out.y, matrix); - out.x = pt.x; - out.y = pt.y; - } - - out.x *= current_zoom; - out.y *= current_zoom; - - return out; - } - - function hideCursor() { - if(cursor) { - cursor.setAttribute('visibility', 'hidden'); - } - } - - function selectAll(evt) { - setSelection(0, curtext.textContent.length); - $(this).unbind(evt); - } - - function selectWord(evt) { - if(!allow_dbl || !curtext) return; - - var ept = transformPoint( evt.pageX, evt.pageY, root_sctm ), - mouse_x = ept.x * current_zoom, - mouse_y = ept.y * current_zoom; - var pt = screenToPt(mouse_x, mouse_y); - - var index = getIndexFromPoint(pt.x, pt.y); - var str = curtext.textContent; - var first = str.substr(0, index).replace(/[a-z0-9]+$/i, '').length; - var m = str.substr(index).match(/^[a-z0-9]+/i); - var last = (m?m[0].length:0) + index; - setSelection(first, last); - - // Set tripleclick - $(evt.target).click(selectAll); - setTimeout(function() { - $(evt.target).unbind('click', selectAll); - }, 300); - - } - - return { - select: function(target, x, y) { - curtext = target; - textActions.toEditMode(x, y); - }, - start: function(elem) { - curtext = elem; - textActions.toEditMode(); - }, - mouseDown: function(evt, mouse_target, start_x, start_y) { - var pt = screenToPt(start_x, start_y); - - textinput.focus(); - setCursorFromPoint(pt.x, pt.y); - last_x = start_x; - last_y = start_y; - - // TODO: Find way to block native selection - }, - mouseMove: function(mouse_x, mouse_y) { - var pt = screenToPt(mouse_x, mouse_y); - setEndSelectionFromPoint(pt.x, pt.y); - }, - mouseUp: function(evt, mouse_x, mouse_y) { - var pt = screenToPt(mouse_x, mouse_y); - - setEndSelectionFromPoint(pt.x, pt.y, true); - - // TODO: Find a way to make this work: Use transformed BBox instead of evt.target -// if(last_x === mouse_x && last_y === mouse_y -// && !svgedit.math.rectsIntersect(transbb, {x: pt.x, y: pt.y, width:0, height:0})) { -// textActions.toSelectMode(true); -// } - - if( - evt.target !== curtext - && mouse_x < last_x + 2 - && mouse_x > last_x - 2 - && mouse_y < last_y + 2 - && mouse_y > last_y - 2) { - - textActions.toSelectMode(true); - } - - }, - setCursor: setCursor, - toEditMode: function(x, y) { - allow_dbl = false; - current_mode = "textedit"; - selectorManager.requestSelector(curtext).showGrips(false); - // Make selector group accept clicks - var sel = selectorManager.requestSelector(curtext).selectorRect; - - textActions.init(); - - $(curtext).css('cursor', 'text'); - -// if(svgedit.browser.supportsEditableText()) { -// curtext.setAttribute('editable', 'simple'); -// return; -// } - - if(!arguments.length) { - setCursor(); - } else { - var pt = screenToPt(x, y); - setCursorFromPoint(pt.x, pt.y); - } - - setTimeout(function() { - allow_dbl = true; - }, 300); - }, - toSelectMode: function(selectElem) { - current_mode = "select"; - clearInterval(blinker); - blinker = null; - if(selblock) $(selblock).attr('display','none'); - if(cursor) $(cursor).attr('visibility','hidden'); - $(curtext).css('cursor', 'move'); - - if(selectElem) { - clearSelection(); - $(curtext).css('cursor', 'move'); - - call("selected", [curtext]); - addToSelection([curtext], true); - } - if(curtext && !curtext.textContent.length) { - // No content, so delete - canvas.deleteSelectedElements(); - } - - $(textinput).blur(); - - curtext = false; - -// if(svgedit.browser.supportsEditableText()) { -// curtext.removeAttribute('editable'); -// } - }, - setInputElem: function(elem) { - textinput = elem; -// $(textinput).blur(hideCursor); - }, - clear: function() { - if(current_mode == "textedit") { - textActions.toSelectMode(); - } - }, - init: function(inputElem) { - if(!curtext) return; - -// if(svgedit.browser.supportsEditableText()) { -// curtext.select(); -// return; -// } - - if(!curtext.parentNode) { - // Result of the ffClone, need to get correct element - curtext = selectedElements[0]; - selectorManager.requestSelector(curtext).showGrips(false); - } - - var str = curtext.textContent; - var len = str.length; - - var xform = curtext.getAttribute('transform'); - - textbb = svgedit.utilities.getBBox(curtext); - - matrix = xform?getMatrix(curtext):null; - - chardata = Array(len); - textinput.focus(); - - $(curtext).unbind('dblclick', selectWord).dblclick(selectWord); - - if(!len) { - var end = {x: textbb.x + (textbb.width/2), width: 0}; - } - - for(var i=0; i<len; i++) { - var start = curtext.getStartPositionOfChar(i); - var end = curtext.getEndPositionOfChar(i); - - if(!svgedit.browser.supportsGoodTextCharPos()) { - var offset = canvas.contentW * current_zoom; - start.x -= offset; - end.x -= offset; - - start.x /= current_zoom; - end.x /= current_zoom; - } - - // Get a "bbox" equivalent for each character. Uses the - // bbox data of the actual text for y, height purposes - - // TODO: Decide if y, width and height are actually necessary - chardata[i] = { - x: start.x, - y: textbb.y, // start.y? - width: end.x - start.x, - height: textbb.height - }; - } - - // Add a last bbox for cursor at end of text - chardata.push({ - x: end.x, - width: 0 - }); - setSelection(textinput.selectionStart, textinput.selectionEnd, true); - } - } -}(); - -// TODO: Migrate all of this code into path.js -// Group: Path edit functions -// Functions relating to editing path elements -var pathActions = canvas.pathActions = function() { - - var subpath = false; - var current_path; - var newPoint, firstCtrl; - - function resetD(p) { - p.setAttribute("d", pathActions.convertPath(p)); - } - - // TODO: Move into path.js - svgedit.path.Path.prototype.endChanges = function(text) { - if(svgedit.browser.isWebkit()) resetD(this.elem); - var cmd = new ChangeElementCommand(this.elem, {d: this.last_d}, text); - addCommandToHistory(cmd); - call("changed", [this.elem]); - } - - svgedit.path.Path.prototype.addPtsToSelection = function(indexes) { - if(!$.isArray(indexes)) indexes = [indexes]; - for(var i=0; i< indexes.length; i++) { - var index = indexes[i]; - var seg = this.segs[index]; - if(seg.ptgrip) { - if(this.selected_pts.indexOf(index) == -1 && index >= 0) { - this.selected_pts.push(index); - } - } - }; - this.selected_pts.sort(); - var i = this.selected_pts.length, - grips = new Array(i); - // Loop through points to be selected and highlight each - while(i--) { - var pt = this.selected_pts[i]; - var seg = this.segs[pt]; - seg.select(true); - grips[i] = seg.ptgrip; - } - // TODO: Correct this: - pathActions.canDeleteNodes = true; - - pathActions.closed_subpath = this.subpathIsClosed(this.selected_pts[0]); - - call("selected", grips); - } - - var current_path = null, - drawn_path = null, - hasMoved = false; - - // This function converts a polyline (created by the fh_path tool) into - // a path element and coverts every three line segments into a single bezier - // curve in an attempt to smooth out the free-hand - var smoothPolylineIntoPath = function(element) { - var points = element.points; - var N = points.numberOfItems; - if (N >= 4) { - // loop through every 3 points and convert to a cubic bezier curve segment - // - // NOTE: this is cheating, it means that every 3 points has the potential to - // be a corner instead of treating each point in an equal manner. In general, - // this technique does not look that good. - // - // I am open to better ideas! - // - // Reading: - // - http://www.efg2.com/Lab/Graphics/Jean-YvesQueinecBezierCurves.htm - // - http://www.codeproject.com/KB/graphics/BezierSpline.aspx?msg=2956963 - // - http://www.ian-ko.com/ET_GeoWizards/UserGuide/smooth.htm - // - http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html - var curpos = points.getItem(0), prevCtlPt = null; - var d = []; - d.push(["M",curpos.x,",",curpos.y," C"].join("")); - for (var i = 1; i <= (N-4); i += 3) { - var ct1 = points.getItem(i); - var ct2 = points.getItem(i+1); - var end = points.getItem(i+2); - - // if the previous segment had a control point, we want to smooth out - // the control points on both sides - if (prevCtlPt) { - var newpts = svgedit.path.smoothControlPoints( prevCtlPt, ct1, curpos ); - if (newpts && newpts.length == 2) { - var prevArr = d[d.length-1].split(','); - prevArr[2] = newpts[0].x; - prevArr[3] = newpts[0].y; - d[d.length-1] = prevArr.join(','); - ct1 = newpts[1]; - } - } - - d.push([ct1.x,ct1.y,ct2.x,ct2.y,end.x,end.y].join(',')); - - curpos = end; - prevCtlPt = ct2; - } - // handle remaining line segments - d.push("L"); - for(;i < N;++i) { - var pt = points.getItem(i); - d.push([pt.x,pt.y].join(",")); - } - d = d.join(" "); - - // create new path element - element = addSvgElementFromJson({ - "element": "path", - "curStyles": true, - "attr": { - "id": getId(), - "d": d, - "fill": "none" - } - }); - // No need to call "changed", as this is already done under mouseUp - } - return element; - }; - - return { - mouseDown: function(evt, mouse_target, start_x, start_y) { - if(current_mode === "path") { - mouse_x = start_x; - mouse_y = start_y; - - var x = mouse_x/current_zoom, - y = mouse_y/current_zoom, - stretchy = getElem("path_stretch_line"); - newPoint = [x, y]; - - if(curConfig.gridSnapping){ - x = snapToGrid(x); - y = snapToGrid(y); - mouse_x = snapToGrid(mouse_x); - mouse_y = snapToGrid(mouse_y); - } - - if (!stretchy) { - stretchy = document.createElementNS(svgns, "path"); - assignAttributes(stretchy, { - 'id': "path_stretch_line", - 'stroke': "#22C", - 'stroke-width': "0.5", - 'fill': 'none' - }); - stretchy = getElem("selectorParentGroup").appendChild(stretchy); - } - stretchy.setAttribute("display", "inline"); - - var keep = null; - - // if pts array is empty, create path element with M at current point - if (!drawn_path) { - d_attr = "M" + x + "," + y + " "; - drawn_path = addSvgElementFromJson({ - "element": "path", - "curStyles": true, - "attr": { - "d": d_attr, - "id": getNextId(), - "opacity": cur_shape.opacity - } - }); - // set stretchy line to first point - stretchy.setAttribute('d', ['M', mouse_x, mouse_y, mouse_x, mouse_y].join(' ')); - var index = subpath ? svgedit.path.path.segs.length : 0; - svgedit.path.addPointGrip(index, mouse_x, mouse_y); - } - else { - // determine if we clicked on an existing point - var seglist = drawn_path.pathSegList; - var i = seglist.numberOfItems; - var FUZZ = 6/current_zoom; - var clickOnPoint = false; - while(i) { - i --; - var item = seglist.getItem(i); - var px = item.x, py = item.y; - // found a matching point - if ( x >= (px-FUZZ) && x <= (px+FUZZ) && y >= (py-FUZZ) && y <= (py+FUZZ) ) { - clickOnPoint = true; - break; - } - } - - // get path element that we are in the process of creating - var id = getId(); - - // Remove previous path object if previously created - svgedit.path.removePath_(id); - - var newpath = getElem(id); - - var len = seglist.numberOfItems; - // if we clicked on an existing point, then we are done this path, commit it - // (i,i+1) are the x,y that were clicked on - if (clickOnPoint) { - // if clicked on any other point but the first OR - // the first point was clicked on and there are less than 3 points - // then leave the path open - // otherwise, close the path - if (i <= 1 && len >= 2) { - // Create end segment - var abs_x = seglist.getItem(0).x; - var abs_y = seglist.getItem(0).y; - - - var s_seg = stretchy.pathSegList.getItem(1); - if(s_seg.pathSegType === 4) { - var newseg = drawn_path.createSVGPathSegLinetoAbs(abs_x, abs_y); - } else { - var newseg = drawn_path.createSVGPathSegCurvetoCubicAbs( - abs_x, - abs_y, - s_seg.x1 / current_zoom, - s_seg.y1 / current_zoom, - abs_x, - abs_y - ); - } - - var endseg = drawn_path.createSVGPathSegClosePath(); - seglist.appendItem(newseg); - seglist.appendItem(endseg); - } else if(len < 3) { - keep = false; - return keep; - } - $(stretchy).remove(); - - // this will signal to commit the path - element = newpath; - drawn_path = null; - started = false; - - if(subpath) { - if(svgedit.path.path.matrix) { - remapElement(newpath, {}, svgedit.path.path.matrix.inverse()); - } - - var new_d = newpath.getAttribute("d"); - var orig_d = $(svgedit.path.path.elem).attr("d"); - $(svgedit.path.path.elem).attr("d", orig_d + new_d); - $(newpath).remove(); - if(svgedit.path.path.matrix) { - svgedit.path.recalcRotatedPath(); - } - svgedit.path.path.init(); - pathActions.toEditMode(svgedit.path.path.elem); - svgedit.path.path.selectPt(); - return false; - } - } - // else, create a new point, update path element - else { - // Checks if current target or parents are #svgcontent - if(!$.contains(container, getMouseTarget(evt))) { - // Clicked outside canvas, so don't make point - console.log("Clicked outside canvas"); - return false; - } - - var num = drawn_path.pathSegList.numberOfItems; - var last = drawn_path.pathSegList.getItem(num -1); - var lastx = last.x, lasty = last.y; - - if(evt.shiftKey) { var xya = snapToAngle(lastx,lasty,x,y); x=xya.x; y=xya.y; } - - // Use the segment defined by stretchy - var s_seg = stretchy.pathSegList.getItem(1); - if(s_seg.pathSegType === 4) { - var newseg = drawn_path.createSVGPathSegLinetoAbs(round(x), round(y)); - } else { - var newseg = drawn_path.createSVGPathSegCurvetoCubicAbs( - round(x), - round(y), - s_seg.x1 / current_zoom, - s_seg.y1 / current_zoom, - s_seg.x2 / current_zoom, - s_seg.y2 / current_zoom - ); - } - - drawn_path.pathSegList.appendItem(newseg); - - x *= current_zoom; - y *= current_zoom; - - // set stretchy line to latest point - stretchy.setAttribute('d', ['M', x, y, x, y].join(' ')); - var index = num; - if(subpath) index += svgedit.path.path.segs.length; - svgedit.path.addPointGrip(index, x, y); - } -// keep = true; - } - - return; - } - - // TODO: Make sure current_path isn't null at this point - if(!svgedit.path.path) return; - - svgedit.path.path.storeD(); - - var id = evt.target.id; - if (id.substr(0,14) == "pathpointgrip_") { - // Select this point - var cur_pt = svgedit.path.path.cur_pt = parseInt(id.substr(14)); - svgedit.path.path.dragging = [start_x, start_y]; - var seg = svgedit.path.path.segs[cur_pt]; - - // only clear selection if shift is not pressed (otherwise, add - // node to selection) - if (!evt.shiftKey) { - if(svgedit.path.path.selected_pts.length <= 1 || !seg.selected) { - svgedit.path.path.clearSelection(); - } - svgedit.path.path.addPtsToSelection(cur_pt); - } else if(seg.selected) { - svgedit.path.path.removePtFromSelection(cur_pt); - } else { - svgedit.path.path.addPtsToSelection(cur_pt); - } - } else if(id.indexOf("ctrlpointgrip_") == 0) { - svgedit.path.path.dragging = [start_x, start_y]; - - var parts = id.split('_')[1].split('c'); - var cur_pt = parts[0]-0; - var ctrl_num = parts[1]-0; - svgedit.path.path.selectPt(cur_pt, ctrl_num); - } - - // Start selection box - if(!svgedit.path.path.dragging) { - if (rubberBox == null) { - rubberBox = selectorManager.getRubberBandBox(); - } - assignAttributes(rubberBox, { - 'x': start_x * current_zoom, - 'y': start_y * current_zoom, - 'width': 0, - 'height': 0, - 'display': 'inline' - }, 100); - } - }, - mouseMove: function(mouse_x, mouse_y) { - hasMoved = true; - if(current_mode === "path") { - if(!drawn_path) return; - var seglist = drawn_path.pathSegList; - var index = seglist.numberOfItems - 1; - - if(newPoint) { - // First point -// if(!index) return; - - // Set control points - var pointGrip1 = svgedit.path.addCtrlGrip('1c1'); - var pointGrip2 = svgedit.path.addCtrlGrip('0c2'); - - // dragging pointGrip1 - pointGrip1.setAttribute('cx', mouse_x); - pointGrip1.setAttribute('cy', mouse_y); - pointGrip1.setAttribute('display', 'inline'); - - var pt_x = newPoint[0]; - var pt_y = newPoint[1]; - - // set curve - var seg = seglist.getItem(index); - var cur_x = mouse_x / current_zoom; - var cur_y = mouse_y / current_zoom; - var alt_x = (pt_x + (pt_x - cur_x)); - var alt_y = (pt_y + (pt_y - cur_y)); - - pointGrip2.setAttribute('cx', alt_x * current_zoom); - pointGrip2.setAttribute('cy', alt_y * current_zoom); - pointGrip2.setAttribute('display', 'inline'); - - var ctrlLine = svgedit.path.getCtrlLine(1); - assignAttributes(ctrlLine, { - x1: mouse_x, - y1: mouse_y, - x2: alt_x * current_zoom, - y2: alt_y * current_zoom, - display: 'inline' - }); - - if(index === 0) { - firstCtrl = [mouse_x, mouse_y]; - } else { - var last_x, last_y; - - var last = seglist.getItem(index - 1); - var last_x = last.x; - var last_y = last.y - - if(last.pathSegType === 6) { - last_x += (last_x - last.x2); - last_y += (last_y - last.y2); - } else if(firstCtrl) { - last_x = firstCtrl[0]/current_zoom; - last_y = firstCtrl[1]/current_zoom; - } - svgedit.path.replacePathSeg(6, index, [pt_x, pt_y, last_x, last_y, alt_x, alt_y], drawn_path); - } - } else { - var stretchy = getElem("path_stretch_line"); - if (stretchy) { - var prev = seglist.getItem(index); - if(prev.pathSegType === 6) { - var prev_x = prev.x + (prev.x - prev.x2); - var prev_y = prev.y + (prev.y - prev.y2); - svgedit.path.replacePathSeg(6, 1, [mouse_x, mouse_y, prev_x * current_zoom, prev_y * current_zoom, mouse_x, mouse_y], stretchy); - } else if(firstCtrl) { - svgedit.path.replacePathSeg(6, 1, [mouse_x, mouse_y, firstCtrl[0], firstCtrl[1], mouse_x, mouse_y], stretchy); - } else { - svgedit.path.replacePathSeg(4, 1, [mouse_x, mouse_y], stretchy); - } - } - } - return; - } - // if we are dragging a point, let's move it - if (svgedit.path.path.dragging) { - var pt = svgedit.path.getPointFromGrip({ - x: svgedit.path.path.dragging[0], - y: svgedit.path.path.dragging[1] - }, svgedit.path.path); - var mpt = svgedit.path.getPointFromGrip({ - x: mouse_x, - y: mouse_y - }, svgedit.path.path); - var diff_x = mpt.x - pt.x; - var diff_y = mpt.y - pt.y; - svgedit.path.path.dragging = [mouse_x, mouse_y]; - - if(svgedit.path.path.dragctrl) { - svgedit.path.path.moveCtrl(diff_x, diff_y); - } else { - svgedit.path.path.movePts(diff_x, diff_y); - } - } else { - svgedit.path.path.selected_pts = []; - svgedit.path.path.eachSeg(function(i) { - var seg = this; - if(!seg.next && !seg.prev) return; - - var item = seg.item; - var rbb = rubberBox.getBBox(); - - var pt = svgedit.path.getGripPt(seg); - var pt_bb = { - x: pt.x, - y: pt.y, - width: 0, - height: 0 - }; - - var sel = svgedit.math.rectsIntersect(rbb, pt_bb); - - this.select(sel); - //Note that addPtsToSelection is not being run - if(sel) svgedit.path.path.selected_pts.push(seg.index); - }); - - } - }, - mouseUp: function(evt, element, mouse_x, mouse_y) { - - // Create mode - if(current_mode === "path") { - newPoint = null; - if(!drawn_path) { - element = getElem(getId()); - started = false; - firstCtrl = null; - } - - return { - keep: true, - element: element - } - } - - // Edit mode - - if (svgedit.path.path.dragging) { - var last_pt = svgedit.path.path.cur_pt; - - svgedit.path.path.dragging = false; - svgedit.path.path.dragctrl = false; - svgedit.path.path.update(); - - - if(hasMoved) { - svgedit.path.path.endChanges("Move path point(s)"); - } - - if(!evt.shiftKey && !hasMoved) { - svgedit.path.path.selectPt(last_pt); - } - } - else if(rubberBox && rubberBox.getAttribute('display') != 'none') { - // Done with multi-node-select - rubberBox.setAttribute("display", "none"); - - if(rubberBox.getAttribute('width') <= 2 && rubberBox.getAttribute('height') <= 2) { - pathActions.toSelectMode(evt.target); - } - - // else, move back to select mode - } else { - pathActions.toSelectMode(evt.target); - } - hasMoved = false; - }, - toEditMode: function(element) { - svgedit.path.path = svgedit.path.getPath_(element); - current_mode = "pathedit"; - clearSelection(); - svgedit.path.path.show(true).update(); - svgedit.path.path.oldbbox = svgedit.utilities.getBBox(svgedit.path.path.elem); - subpath = false; - }, - toSelectMode: function(elem) { - var selPath = (elem == svgedit.path.path.elem); - current_mode = "select"; - svgedit.path.path.show(false); - current_path = false; - clearSelection(); - - if(svgedit.path.path.matrix) { - // Rotated, so may need to re-calculate the center - svgedit.path.recalcRotatedPath(); - } - - if(selPath) { - call("selected", [elem]); - addToSelection([elem], true); - } - }, - addSubPath: function(on) { - if(on) { - // Internally we go into "path" mode, but in the UI it will - // still appear as if in "pathedit" mode. - current_mode = "path"; - subpath = true; - } else { - pathActions.clear(true); - pathActions.toEditMode(svgedit.path.path.elem); - } - }, - select: function(target) { - if (current_path === target) { - pathActions.toEditMode(target); - current_mode = "pathedit"; - } // going into pathedit mode - else { - current_path = target; - } - }, - reorient: function() { - var elem = selectedElements[0]; - if(!elem) return; - var angle = getRotationAngle(elem); - if(angle == 0) return; - - var batchCmd = new BatchCommand("Reorient path"); - var changes = { - d: elem.getAttribute('d'), - transform: elem.getAttribute('transform') - }; - batchCmd.addSubCommand(new ChangeElementCommand(elem, changes)); - clearSelection(); - this.resetOrientation(elem); - - addCommandToHistory(batchCmd); - - // Set matrix to null - svgedit.path.getPath_(elem).show(false).matrix = null; - - this.clear(); - - addToSelection([elem], true); - call("changed", selectedElements); - }, - - clear: function(remove) { - current_path = null; - if (drawn_path) { - var elem = getElem(getId()); - $(getElem("path_stretch_line")).remove(); - $(elem).remove(); - $(getElem("pathpointgrip_container")).find('*').attr('display', 'none'); - drawn_path = firstCtrl = null; - started = false; - } else if (current_mode == "pathedit") { - this.toSelectMode(); - } - if(svgedit.path.path) svgedit.path.path.init().show(false); - }, - resetOrientation: function(path) { - if(path == null || path.nodeName != 'path') return false; - var tlist = getTransformList(path); - var m = transformListToTransform(tlist).matrix; - tlist.clear(); - path.removeAttribute("transform"); - var segList = path.pathSegList; - - // Opera/win/non-EN throws an error here. - // TODO: Find out why! - // Presumed fixed in Opera 10.5, so commented out for now - -// try { - var len = segList.numberOfItems; -// } catch(err) { -// var fixed_d = pathActions.convertPath(path); -// path.setAttribute('d', fixed_d); -// segList = path.pathSegList; -// var len = segList.numberOfItems; -// } - var last_x, last_y; - - - for (var i = 0; i < len; ++i) { - var seg = segList.getItem(i); - var type = seg.pathSegType; - if(type == 1) continue; - var pts = []; - $.each(['',1,2], function(j, n) { - var x = seg['x'+n], y = seg['y'+n]; - if(x !== undefined && y !== undefined) { - var pt = transformPoint(x, y, m); - pts.splice(pts.length, 0, pt.x, pt.y); - } - }); - svgedit.path.replacePathSeg(type, i, pts, path); - } - - reorientGrads(path, m); - - - }, - zoomChange: function() { - if(current_mode == "pathedit") { - svgedit.path.path.update(); - } - }, - getNodePoint: function() { - var sel_pt = svgedit.path.path.selected_pts.length ? svgedit.path.path.selected_pts[0] : 1; - - var seg = svgedit.path.path.segs[sel_pt]; - return { - x: seg.item.x, - y: seg.item.y, - type: seg.type - }; - }, - linkControlPoints: function(linkPoints) { - svgedit.path.setLinkControlPoints(linkPoints); - }, - clonePathNode: function() { - svgedit.path.path.storeD(); - - var sel_pts = svgedit.path.path.selected_pts; - var segs = svgedit.path.path.segs; - - var i = sel_pts.length; - var nums = []; - - while(i--) { - var pt = sel_pts[i]; - svgedit.path.path.addSeg(pt); - - nums.push(pt + i); - nums.push(pt + i + 1); - } - svgedit.path.path.init().addPtsToSelection(nums); - - svgedit.path.path.endChanges("Clone path node(s)"); - }, - opencloseSubPath: function() { - var sel_pts = svgedit.path.path.selected_pts; - // Only allow one selected node for now - if(sel_pts.length !== 1) return; - - var elem = svgedit.path.path.elem; - var list = elem.pathSegList; - - var len = list.numberOfItems; - - var index = sel_pts[0]; - - var open_pt = null; - var start_item = null; - - // Check if subpath is already open - svgedit.path.path.eachSeg(function(i) { - if(this.type === 2 && i <= index) { - start_item = this.item; - } - if(i <= index) return true; - if(this.type === 2) { - // Found M first, so open - open_pt = i; - return false; - } else if(this.type === 1) { - // Found Z first, so closed - open_pt = false; - return false; - } - }); - - if(open_pt == null) { - // Single path, so close last seg - open_pt = svgedit.path.path.segs.length - 1; - } - - if(open_pt !== false) { - // Close this path - - // Create a line going to the previous "M" - var newseg = elem.createSVGPathSegLinetoAbs(start_item.x, start_item.y); - - var closer = elem.createSVGPathSegClosePath(); - if(open_pt == svgedit.path.path.segs.length - 1) { - list.appendItem(newseg); - list.appendItem(closer); - } else { - svgedit.path.insertItemBefore(elem, closer, open_pt); - svgedit.path.insertItemBefore(elem, newseg, open_pt); - } - - svgedit.path.path.init().selectPt(open_pt+1); - return; - } - - - - // M 1,1 L 2,2 L 3,3 L 1,1 z // open at 2,2 - // M 2,2 L 3,3 L 1,1 - - // M 1,1 L 2,2 L 1,1 z M 4,4 L 5,5 L6,6 L 5,5 z - // M 1,1 L 2,2 L 1,1 z [M 4,4] L 5,5 L(M)6,6 L 5,5 z - - var seg = svgedit.path.path.segs[index]; - - if(seg.mate) { - list.removeItem(index); // Removes last "L" - list.removeItem(index); // Removes the "Z" - svgedit.path.path.init().selectPt(index - 1); - return; - } - - var last_m, z_seg; - - // Find this sub-path's closing point and remove - for(var i=0; i<list.numberOfItems; i++) { - var item = list.getItem(i); - - if(item.pathSegType === 2) { - // Find the preceding M - last_m = i; - } else if(i === index) { - // Remove it - list.removeItem(last_m); -// index--; - } else if(item.pathSegType === 1 && index < i) { - // Remove the closing seg of this subpath - z_seg = i-1; - list.removeItem(i); - break; - } - } - - var num = (index - last_m) - 1; - - while(num--) { - svgedit.path.insertItemBefore(elem, list.getItem(last_m), z_seg); - } - - var pt = list.getItem(last_m); - - // Make this point the new "M" - svgedit.path.replacePathSeg(2, last_m, [pt.x, pt.y]); - - var i = index - - svgedit.path.path.init().selectPt(0); - }, - deletePathNode: function() { - if(!pathActions.canDeleteNodes) return; - svgedit.path.path.storeD(); - - var sel_pts = svgedit.path.path.selected_pts; - var i = sel_pts.length; - - while(i--) { - var pt = sel_pts[i]; - svgedit.path.path.deleteSeg(pt); - } - - // Cleanup - var cleanup = function() { - var segList = svgedit.path.path.elem.pathSegList; - var len = segList.numberOfItems; - - var remItems = function(pos, count) { - while(count--) { - segList.removeItem(pos); - } - } - - if(len <= 1) return true; - - while(len--) { - var item = segList.getItem(len); - if(item.pathSegType === 1) { - var prev = segList.getItem(len-1); - var nprev = segList.getItem(len-2); - if(prev.pathSegType === 2) { - remItems(len-1, 2); - cleanup(); - break; - } else if(nprev.pathSegType === 2) { - remItems(len-2, 3); - cleanup(); - break; - } - - } else if(item.pathSegType === 2) { - if(len > 0) { - var prev_type = segList.getItem(len-1).pathSegType; - // Path has M M - if(prev_type === 2) { - remItems(len-1, 1); - cleanup(); - break; - // Entire path ends with Z M - } else if(prev_type === 1 && segList.numberOfItems-1 === len) { - remItems(len, 1); - cleanup(); - break; - } - } - } - } - return false; - } - - cleanup(); - - // Completely delete a path with 1 or 0 segments - if(svgedit.path.path.elem.pathSegList.numberOfItems <= 1) { - pathActions.toSelectMode(svgedit.path.path.elem); - canvas.deleteSelectedElements(); - return; - } - - svgedit.path.path.init(); - - svgedit.path.path.clearSelection(); - - // TODO: Find right way to select point now - // path.selectPt(sel_pt); - if(window.opera) { // Opera repaints incorrectly - var cp = $(svgedit.path.path.elem); cp.attr('d',cp.attr('d')); - } - svgedit.path.path.endChanges("Delete path node(s)"); - }, - smoothPolylineIntoPath: smoothPolylineIntoPath, - setSegType: function(v) { - svgedit.path.path.setSegType(v); - }, - moveNode: function(attr, newValue) { - var sel_pts = svgedit.path.path.selected_pts; - if(!sel_pts.length) return; - - svgedit.path.path.storeD(); - - // Get first selected point - var seg = svgedit.path.path.segs[sel_pts[0]]; - var diff = {x:0, y:0}; - diff[attr] = newValue - seg.item[attr]; - - seg.move(diff.x, diff.y); - svgedit.path.path.endChanges("Move path point"); - }, - fixEnd: function(elem) { - // Adds an extra segment if the last seg before a Z doesn't end - // at its M point - // M0,0 L0,100 L100,100 z - var segList = elem.pathSegList; - var len = segList.numberOfItems; - var last_m; - for (var i = 0; i < len; ++i) { - var item = segList.getItem(i); - if(item.pathSegType === 2) { - last_m = item; - } - - if(item.pathSegType === 1) { - var prev = segList.getItem(i-1); - if(prev.x != last_m.x || prev.y != last_m.y) { - // Add an L segment here - var newseg = elem.createSVGPathSegLinetoAbs(last_m.x, last_m.y); - svgedit.path.insertItemBefore(elem, newseg, i); - // Can this be done better? - pathActions.fixEnd(elem); - break; - } - - } - } - if(svgedit.browser.isWebkit()) resetD(elem); - }, - // Convert a path to one with only absolute or relative values - convertPath: function(path, toRel) { - var segList = path.pathSegList; - var len = segList.numberOfItems; - var curx = 0, cury = 0; - var d = ""; - var last_m = null; - - for (var i = 0; i < len; ++i) { - var seg = segList.getItem(i); - // if these properties are not in the segment, set them to zero - var x = seg.x || 0, - y = seg.y || 0, - x1 = seg.x1 || 0, - y1 = seg.y1 || 0, - x2 = seg.x2 || 0, - y2 = seg.y2 || 0; - - var type = seg.pathSegType; - var letter = pathMap[type]['to'+(toRel?'Lower':'Upper')+'Case'](); - - var addToD = function(pnts, more, last) { - var str = ''; - var more = more?' '+more.join(' '):''; - var last = last?' '+svgedit.units.shortFloat(last):''; - $.each(pnts, function(i, pnt) { - pnts[i] = svgedit.units.shortFloat(pnt); - }); - d += letter + pnts.join(' ') + more + last; - } - - switch (type) { - case 1: // z,Z closepath (Z/z) - d += "z"; - break; - case 12: // absolute horizontal line (H) - x -= curx; - case 13: // relative horizontal line (h) - if(toRel) { - curx += x; - letter = 'l'; - } else { - x += curx; - curx = x; - letter = 'L'; - } - // Convert to "line" for easier editing - addToD([[x, cury]]); - break; - case 14: // absolute vertical line (V) - y -= cury; - case 15: // relative vertical line (v) - if(toRel) { - cury += y; - letter = 'l'; - } else { - y += cury; - cury = y; - letter = 'L'; - } - // Convert to "line" for easier editing - addToD([[curx, y]]); - break; - case 2: // absolute move (M) - case 4: // absolute line (L) - case 18: // absolute smooth quad (T) - x -= curx; - y -= cury; - case 5: // relative line (l) - case 3: // relative move (m) - // If the last segment was a "z", this must be relative to - if(last_m && segList.getItem(i-1).pathSegType === 1 && !toRel) { - curx = last_m[0]; - cury = last_m[1]; - } - - case 19: // relative smooth quad (t) - if(toRel) { - curx += x; - cury += y; - } else { - x += curx; - y += cury; - curx = x; - cury = y; - } - if(type === 3) last_m = [curx, cury]; - - addToD([[x,y]]); - break; - case 6: // absolute cubic (C) - x -= curx; x1 -= curx; x2 -= curx; - y -= cury; y1 -= cury; y2 -= cury; - case 7: // relative cubic (c) - if(toRel) { - curx += x; - cury += y; - } else { - x += curx; x1 += curx; x2 += curx; - y += cury; y1 += cury; y2 += cury; - curx = x; - cury = y; - } - addToD([[x1,y1],[x2,y2],[x,y]]); - break; - case 8: // absolute quad (Q) - x -= curx; x1 -= curx; - y -= cury; y1 -= cury; - case 9: // relative quad (q) - if(toRel) { - curx += x; - cury += y; - } else { - x += curx; x1 += curx; - y += cury; y1 += cury; - curx = x; - cury = y; - } - addToD([[x1,y1],[x,y]]); - break; - case 10: // absolute elliptical arc (A) - x -= curx; - y -= cury; - case 11: // relative elliptical arc (a) - if(toRel) { - curx += x; - cury += y; - } else { - x += curx; - y += cury; - curx = x; - cury = y; - } - addToD([[seg.r1,seg.r2]], [ - seg.angle, - (seg.largeArcFlag ? 1 : 0), - (seg.sweepFlag ? 1 : 0) - ],[x,y] - ); - break; - case 16: // absolute smooth cubic (S) - x -= curx; x2 -= curx; - y -= cury; y2 -= cury; - case 17: // relative smooth cubic (s) - if(toRel) { - curx += x; - cury += y; - } else { - x += curx; x2 += curx; - y += cury; y2 += cury; - curx = x; - cury = y; - } - addToD([[x2,y2],[x,y]]); - break; - } // switch on path segment type - } // for each segment - return d; - } - } -}(); -// end pathActions - -// Group: Serialization - -// Function: removeUnusedDefElems -// Looks at DOM elements inside the <defs> to see if they are referred to, -// removes them from the DOM if they are not. -// -// Returns: -// The amount of elements that were removed -var removeUnusedDefElems = this.removeUnusedDefElems = function() { - var defs = svgcontent.getElementsByTagNameNS(svgns, "defs"); - if(!defs || !defs.length) return 0; - -// if(!defs.firstChild) return; - - var defelem_uses = [], - numRemoved = 0; - var attrs = ['fill', 'stroke', 'filter', 'marker-start', 'marker-mid', 'marker-end']; - var alen = attrs.length; - - var all_els = svgcontent.getElementsByTagNameNS(svgns, '*'); - var all_len = all_els.length; - - for(var i=0; i<all_len; i++) { - var el = all_els[i]; - for(var j = 0; j < alen; j++) { - var ref = getUrlFromAttr(el.getAttribute(attrs[j])); - if(ref) { - defelem_uses.push(ref.substr(1)); - } - } - - // gradients can refer to other gradients - var href = getHref(el); - if (href && href.indexOf('#') === 0) { - defelem_uses.push(href.substr(1)); - } - }; - - var defelems = $(defs).find("linearGradient, radialGradient, filter, marker, svg, symbol"); - defelem_ids = [], - i = defelems.length; - while (i--) { - var defelem = defelems[i]; - var id = defelem.id; - if(defelem_uses.indexOf(id) < 0) { - // Not found, so remove (but remember) - removedElements[id] = defelem; - defelem.parentNode.removeChild(defelem); - numRemoved++; - } - } - - return numRemoved; -} - -// Function: svgCanvasToString -// Main function to set up the SVG content for output -// -// Returns: -// String containing the SVG image for output -this.svgCanvasToString = function() { - // keep calling it until there are none to remove - while (removeUnusedDefElems() > 0) {}; - - pathActions.clear(true); - - // Keep SVG-Edit comment on top - $.each(svgcontent.childNodes, function(i, node) { - if(i && node.nodeType === 8 && node.data.indexOf('Created with') >= 0) { - svgcontent.insertBefore(node, svgcontent.firstChild); - } - }); - - // Move out of in-group editing mode - if(current_group) { - leaveContext(); - selectOnly([current_group]); - } - - var naked_svgs = []; - - // Unwrap gsvg if it has no special attributes (only id and style) - $(svgcontent).find('g:data(gsvg)').each(function() { - var attrs = this.attributes; - var len = attrs.length; - for(var i=0; i<len; i++) { - if(attrs[i].nodeName == 'id' || attrs[i].nodeName == 'style') { - len--; - } - } - // No significant attributes, so ungroup - if(len <= 0) { - var svg = this.firstChild; - naked_svgs.push(svg); - $(this).replaceWith(svg); - } - }); - var output = this.svgToString(svgcontent, 0); - - // Rewrap gsvg - if(naked_svgs.length) { - $(naked_svgs).each(function() { - groupSvgElem(this); - }); - } - - return output; -}; - -// Function: svgToString -// Sub function ran on each SVG element to convert it to a string as desired -// -// Parameters: -// elem - The SVG element to convert -// indent - Integer with the amount of spaces to indent this tag -// -// Returns: -// String with the given element as an SVG tag -this.svgToString = function(elem, indent) { - var out = new Array(), toXml = svgedit.utilities.toXml; - var unit = curConfig.baseUnit; - var unit_re = new RegExp('^-?[\\d\\.]+' + unit + '$'); - - if (elem) { - cleanupElement(elem); - var attrs = elem.attributes, - attr, - i, - childs = elem.childNodes; - - for (var i=0; i<indent; i++) out.push(" "); - out.push("<"); out.push(elem.nodeName); - if(elem.id === 'svgcontent') { - // Process root element separately - var res = getResolution(); - - var vb = ""; - // TODO: Allow this by dividing all values by current baseVal - // Note that this also means we should properly deal with this on import -// if(curConfig.baseUnit !== "px") { -// var unit = curConfig.baseUnit; -// var unit_m = svgedit.units.getTypeMap()[unit]; -// res.w = svgedit.units.shortFloat(res.w / unit_m) -// res.h = svgedit.units.shortFloat(res.h / unit_m) -// vb = ' viewBox="' + [0, 0, res.w, res.h].join(' ') + '"'; -// res.w += unit; -// res.h += unit; -// } - - if(unit !== "px") { - res.w = Math.round(svgedit.units.convertUnit(res.w, unit) + unit); - res.h = Math.round(svgedit.units.convertUnit(res.h, unit) + unit); - } - - out.push(' width="' + res.w + '" height="' + res.h + '"' + vb + ' xmlns="'+svgns+'"'); - - var nsuris = {}; - - // Check elements for namespaces, add if found - $(elem).find('*').andSelf().each(function() { - var el = this; - $.each(this.attributes, function(i, attr) { - var uri = attr.namespaceURI; - if(uri && !nsuris[uri] && nsMap[uri] !== 'xmlns' && nsMap[uri] !== 'xml' ) { - nsuris[uri] = true; - out.push(" xmlns:" + nsMap[uri] + '="' + uri +'"'); - } - }); - }); - - var i = attrs.length; - var attr_names = ['width','height','xmlns','x','y','viewBox','id','overflow']; - while (i--) { - attr = attrs.item(i); - var attrVal = toXml(attr.nodeValue); - - // Namespaces have already been dealt with, so skip - if(attr.nodeName.indexOf('xmlns:') === 0) continue; - - // only serialize attributes we don't use internally - if (attrVal != "" && attr_names.indexOf(attr.localName) == -1) - { - - if(!attr.namespaceURI || nsMap[attr.namespaceURI]) { - out.push(' '); - out.push(attr.nodeName); out.push("=\""); - out.push(attrVal); out.push("\""); - } - } - } - } else { - // Skip empty defs - if(elem.nodeName === 'defs' && !elem.firstChild) return; - - var moz_attrs = ['-moz-math-font-style', '_moz-math-font-style']; - for (var i=attrs.length-1; i>=0; i--) { - attr = attrs.item(i); - var attrVal = toXml(attr.nodeValue); - //remove bogus attributes added by Gecko - if (moz_attrs.indexOf(attr.localName) >= 0) continue; - if (attrVal != "") { - if(attrVal.indexOf('pointer-events') === 0) continue; - if(attr.localName === "class" && attrVal.indexOf('se_') === 0) continue; - out.push(" "); - if(attr.localName === 'd') attrVal = pathActions.convertPath(elem, true); - if(!isNaN(attrVal)) { - attrVal = svgedit.units.shortFloat(attrVal); - } else if(unit_re.test(attrVal)) { - attrVal = svgedit.units.shortFloat(attrVal) + unit; - } - - // Embed images when saving - if(save_options.apply - && elem.nodeName === 'image' - && attr.localName === 'href' - && save_options.images - && save_options.images === 'embed') - { - var img = encodableImages[attrVal]; - if(img) attrVal = img; - } - - // map various namespaces to our fixed namespace prefixes - // (the default xmlns attribute itself does not get a prefix) - if(!attr.namespaceURI || attr.namespaceURI == svgns || nsMap[attr.namespaceURI]) { - out.push(attr.nodeName); out.push("=\""); - out.push(attrVal); out.push("\""); - } - } - } - } - - if (elem.hasChildNodes()) { - out.push(">"); - indent++; - var bOneLine = false; - - for (var i=0; i<childs.length; i++) - { - var child = childs.item(i); - switch(child.nodeType) { - case 1: // element node - out.push("\n"); - out.push(this.svgToString(childs.item(i), indent)); - break; - case 3: // text node - var str = child.nodeValue.replace(/^\s+|\s+$/g, ""); - if (str != "") { - bOneLine = true; - out.push(toXml(str) + ""); - } - break; - case 4: // cdata node - out.push("\n"); - out.push(new Array(indent+1).join(" ")); - out.push("<![CDATA["); - out.push(child.nodeValue); - out.push("]]>"); - break; - case 8: // comment - out.push("\n"); - out.push(new Array(indent+1).join(" ")); - out.push("<!--"); - out.push(child.data); - out.push("-->"); - break; - } // switch on node type - } - indent--; - if (!bOneLine) { - out.push("\n"); - for (var i=0; i<indent; i++) out.push(" "); - } - out.push("</"); out.push(elem.nodeName); out.push(">"); - } else { - out.push("/>"); - } - } - return out.join(''); -}; // end svgToString() - -// Function: embedImage -// Converts a given image file to a data URL when possible, then runs a given callback -// -// Parameters: -// val - String with the path/URL of the image -// callback - Optional function to run when image data is found, supplies the -// result (data URL or false) as first parameter. -this.embedImage = function(val, callback) { - - // load in the image and once it's loaded, get the dimensions - $(new Image()).load(function() { - // create a canvas the same size as the raster image - var canvas = document.createElement("canvas"); - canvas.width = this.width; - canvas.height = this.height; - // load the raster image into the canvas - canvas.getContext("2d").drawImage(this,0,0); - // retrieve the data: URL - try { - var urldata = ';svgedit_url=' + encodeURIComponent(val); - urldata = canvas.toDataURL().replace(';base64',urldata+';base64'); - encodableImages[val] = urldata; - } catch(e) { - encodableImages[val] = false; - } - last_good_img_url = val; - if(callback) callback(encodableImages[val]); - }).attr('src',val); -} - -// Function: setGoodImage -// Sets a given URL to be a "last good image" URL -this.setGoodImage = function(val) { - last_good_img_url = val; -} - -this.open = function() { - // Nothing by default, handled by optional widget/extension -}; - -// Function: save -// Serializes the current drawing into SVG XML text and returns it to the 'saved' handler. -// This function also includes the XML prolog. Clients of the SvgCanvas bind their save -// function to the 'saved' event. -// -// Returns: -// Nothing -this.save = function(opts) { - // remove the selected outline before serializing - clearSelection(); - // Update save options if provided - if(opts) $.extend(save_options, opts); - save_options.apply = true; - - // no need for doctype, see http://jwatt.org/svg/authoring/#doctype-declaration - var str = this.svgCanvasToString(); - call("saved", str); -}; - -// Function: rasterExport -// Generates a PNG Data URL based on the current image, then calls "exported" -// with an object including the string and any issues found -this.rasterExport = function() { - // remove the selected outline before serializing - clearSelection(); - - // Check for known CanVG issues - var issues = []; - - // Selector and notice - var issue_list = { - 'feGaussianBlur': uiStrings.exportNoBlur, - 'foreignObject': uiStrings.exportNoforeignObject, - '[stroke-dasharray]': uiStrings.exportNoDashArray - }; - var content = $(svgcontent); - - // Add font/text check if Canvas Text API is not implemented - if(!("font" in $('<canvas>')[0].getContext('2d'))) { - issue_list['text'] = uiStrings.exportNoText; - } - - $.each(issue_list, function(sel, descr) { - if(content.find(sel).length) { - issues.push(descr); - } - }); - - var str = this.svgCanvasToString(); - call("exported", {svg: str, issues: issues}); -}; - -// Function: getSvgString -// Returns the current drawing as raw SVG XML text. -// -// Returns: -// The current drawing as raw SVG XML text. -this.getSvgString = function() { - save_options.apply = false; - return this.svgCanvasToString(); -}; - -// Function: randomizeIds -// This function determines whether to use a nonce in the prefix, when -// generating IDs for future documents in SVG-Edit. -// -// Parameters: -// an opional boolean, which, if true, adds a nonce to the prefix. Thus -// svgCanvas.randomizeIds() <==> svgCanvas.randomizeIds(true) -// -// if you're controlling SVG-Edit externally, and want randomized IDs, call -// this BEFORE calling svgCanvas.setSvgString -// -this.randomizeIds = function() { - if (arguments.length > 0 && arguments[0] == false) { - svgedit.draw.randomizeIds(false, getCurrentDrawing()); - } else { - svgedit.draw.randomizeIds(true, getCurrentDrawing()); - } -}; - -// Function: uniquifyElems -// Ensure each element has a unique ID -// -// Parameters: -// g - The parent element of the tree to give unique IDs -var uniquifyElems = this.uniquifyElems = function(g) { - var ids = {}; - // TODO: Handle markers and connectors. These are not yet re-identified properly - // as their referring elements do not get remapped. - // - // <marker id='se_marker_end_svg_7'/> - // <polyline id='svg_7' se:connector='svg_1 svg_6' marker-end='url(#se_marker_end_svg_7)'/> - // - // Problem #1: if svg_1 gets renamed, we do not update the polyline's se:connector attribute - // Problem #2: if the polyline svg_7 gets renamed, we do not update the marker id nor the polyline's marker-end attribute - var ref_elems = ["filter", "linearGradient", "pattern", "radialGradient", "symbol", "textPath", "use"]; - - svgedit.utilities.walkTree(g, function(n) { - // if it's an element node - if (n.nodeType == 1) { - // and the element has an ID - if (n.id) { - // and we haven't tracked this ID yet - if (!(n.id in ids)) { - // add this id to our map - ids[n.id] = {elem:null, attrs:[], hrefs:[]}; - } - ids[n.id]["elem"] = n; - } - - // now search for all attributes on this element that might refer - // to other elements - $.each(ref_attrs,function(i,attr) { - var attrnode = n.getAttributeNode(attr); - if (attrnode) { - // the incoming file has been sanitized, so we should be able to safely just strip off the leading # - var url = svgedit.utilities.getUrlFromAttr(attrnode.value), - refid = url ? url.substr(1) : null; - if (refid) { - if (!(refid in ids)) { - // add this id to our map - ids[refid] = {elem:null, attrs:[], hrefs:[]}; - } - ids[refid]["attrs"].push(attrnode); - } - } - }); - - // check xlink:href now - var href = svgedit.utilities.getHref(n); - // TODO: what if an <image> or <a> element refers to an element internally? - if(href && ref_elems.indexOf(n.nodeName) >= 0) - { - var refid = href.substr(1); - if (refid) { - if (!(refid in ids)) { - // add this id to our map - ids[refid] = {elem:null, attrs:[], hrefs:[]}; - } - ids[refid]["hrefs"].push(n); - } - } - } - }); - - // in ids, we now have a map of ids, elements and attributes, let's re-identify - for (var oldid in ids) { - if (!oldid) continue; - var elem = ids[oldid]["elem"]; - if (elem) { - var newid = getNextId(); - - // assign element its new id - elem.id = newid; - - // remap all url() attributes - var attrs = ids[oldid]["attrs"]; - var j = attrs.length; - while (j--) { - var attr = attrs[j]; - attr.ownerElement.setAttribute(attr.name, "url(#" + newid + ")"); - } - - // remap all href attributes - var hreffers = ids[oldid]["hrefs"]; - var k = hreffers.length; - while (k--) { - var hreffer = hreffers[k]; - svgedit.utilities.setHref(hreffer, "#"+newid); - } - } - } -} - -// Function setUseData -// Assigns reference data for each use element -var setUseData = this.setUseData = function(parent) { - var elems = $(parent); - - if(parent.tagName !== 'use') { - elems = elems.find('use'); - } - - elems.each(function() { - var id = getHref(this).substr(1); - var ref_elem = getElem(id); - if(!ref_elem) return; - $(this).data('ref', ref_elem); - if(ref_elem.tagName == 'symbol' || ref_elem.tagName == 'svg') { - $(this).data('symbol', ref_elem).data('ref', ref_elem); - } - }); -} - -// Function convertGradients -// Converts gradients from userSpaceOnUse to objectBoundingBox -var convertGradients = this.convertGradients = function(elem) { - var elems = $(elem).find('linearGradient, radialGradient'); - if(!elems.length && svgedit.browser.isWebkit()) { - // Bug in webkit prevents regular *Gradient selector search - elems = $(elem).find('*').filter(function() { - return (this.tagName.indexOf('Gradient') >= 0); - }); - } - - elems.each(function() { - var grad = this; - if($(grad).attr('gradientUnits') === 'userSpaceOnUse') { - // TODO: Support more than one element with this ref by duplicating parent grad - var elems = $(svgcontent).find('[fill="url(#' + grad.id + ')"],[stroke="url(#' + grad.id + ')"]'); - if(!elems.length) return; - - // get object's bounding box - var bb = svgedit.utilities.getBBox(elems[0]); - - // This will occur if the element is inside a <defs> or a <symbol>, - // in which we shouldn't need to convert anyway. - if(!bb) return; - - if(grad.tagName === 'linearGradient') { - var g_coords = $(grad).attr(['x1', 'y1', 'x2', 'y2']); - - // If has transform, convert - var tlist = grad.gradientTransform.baseVal; - if(tlist && tlist.numberOfItems > 0) { - var m = transformListToTransform(tlist).matrix; - var pt1 = transformPoint(g_coords.x1, g_coords.y1, m); - var pt2 = transformPoint(g_coords.x2, g_coords.y2, m); - - g_coords.x1 = pt1.x; - g_coords.y1 = pt1.y; - g_coords.x2 = pt2.x; - g_coords.y2 = pt2.y; - grad.removeAttribute('gradientTransform'); - } - - $(grad).attr({ - x1: (g_coords.x1 - bb.x) / bb.width, - y1: (g_coords.y1 - bb.y) / bb.height, - x2: (g_coords.x2 - bb.x) / bb.width, - y2: (g_coords.y2 - bb.y) / bb.height - }); - grad.removeAttribute('gradientUnits'); - } else { - // Note: radialGradient elements cannot be easily converted - // because userSpaceOnUse will keep circular gradients, while - // objectBoundingBox will x/y scale the gradient according to - // its bbox. - - // For now we'll do nothing, though we should probably have - // the gradient be updated as the element is moved, as - // inkscape/illustrator do. - -// var g_coords = $(grad).attr(['cx', 'cy', 'r']); -// -// $(grad).attr({ -// cx: (g_coords.cx - bb.x) / bb.width, -// cy: (g_coords.cy - bb.y) / bb.height, -// r: g_coords.r -// }); -// -// grad.removeAttribute('gradientUnits'); - } - - - } - }); -} - -// Function: convertToGroup -// Converts selected/given <use> or child SVG element to a group -var convertToGroup = this.convertToGroup = function(elem) { - if(!elem) { - elem = selectedElements[0]; - } - var $elem = $(elem); - - var batchCmd = new BatchCommand(); - - var ts; - - if($elem.data('gsvg')) { - // Use the gsvg as the new group - var svg = elem.firstChild; - var pt = $(svg).attr(['x', 'y']); - - $(elem.firstChild.firstChild).unwrap(); - $(elem).removeData('gsvg'); - - var tlist = getTransformList(elem); - var xform = svgroot.createSVGTransform(); - xform.setTranslate(pt.x, pt.y); - tlist.appendItem(xform); - recalculateDimensions(elem); - call("selected", [elem]); - } else if($elem.data('symbol')) { - elem = $elem.data('symbol'); - - ts = $elem.attr('transform'); - var pos = $elem.attr(['x','y']); - - var vb = elem.getAttribute('viewBox'); - - if(vb) { - var nums = vb.split(' '); - pos.x -= +nums[0]; - pos.y -= +nums[1]; - } - - // Not ideal, but works - ts += " translate(" + (pos.x || 0) + "," + (pos.y || 0) + ")"; - - var prev = $elem.prev(); - - // Remove <use> element - batchCmd.addSubCommand(new RemoveElementCommand($elem[0], $elem[0].nextSibling, $elem[0].parentNode)); - $elem.remove(); - - // See if other elements reference this symbol - var has_more = $(svgcontent).find('use:data(symbol)').length; - - var g = svgdoc.createElementNS(svgns, "g"); - var childs = elem.childNodes; - - for(var i = 0; i < childs.length; i++) { - g.appendChild(childs[i].cloneNode(true)); - } - - // Duplicate the gradients for Gecko, since they weren't included in the <symbol> - if(svgedit.browser.isGecko()) { - var dupeGrads = $(findDefs()).children('linearGradient,radialGradient,pattern').clone(); - $(g).append(dupeGrads); - } - - if (ts) { - g.setAttribute("transform", ts); - } - - var parent = elem.parentNode; - - uniquifyElems(g); - - // Put the dupe gradients back into <defs> (after uniquifying them) - if(svgedit.browser.isGecko()) { - $(findDefs()).append( $(g).find('linearGradient,radialGradient,pattern') ); - } - - // now give the g itself a new id - g.id = getNextId(); - - prev.after(g); - - if(parent) { - if(!has_more) { - // remove symbol/svg element - var nextSibling = elem.nextSibling; - parent.removeChild(elem); - batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent)); - } - batchCmd.addSubCommand(new InsertElementCommand(g)); - } - - setUseData(g); - - if(svgedit.browser.isGecko()) { - convertGradients(findDefs()); - } else { - convertGradients(g); - } - - // recalculate dimensions on the top-level children so that unnecessary transforms - // are removed - svgedit.utilities.walkTreePost(g, function(n){try{recalculateDimensions(n)}catch(e){console.log(e)}}); - - // Give ID for any visible element missing one - $(g).find(visElems).each(function() { - if(!this.id) this.id = getNextId(); - }); - - selectOnly([g]); - - var cm = pushGroupProperties(g, true); - if(cm) { - batchCmd.addSubCommand(cm); - } - - addCommandToHistory(batchCmd); - - } else { - console.log('Unexpected element to ungroup:', elem); - } -} - -// -// Function: setSvgString -// This function sets the current drawing as the input SVG XML. -// -// Parameters: -// xmlString - The SVG as XML text. -// -// Returns: -// This function returns false if the set was unsuccessful, true otherwise. -this.setSvgString = function(xmlString) { - try { - // convert string into XML document - var newDoc = svgedit.utilities.text2xml(xmlString); - - this.prepareSvg(newDoc); - - var batchCmd = new BatchCommand("Change Source"); - - // remove old svg document - var nextSibling = svgcontent.nextSibling; - var oldzoom = svgroot.removeChild(svgcontent); - batchCmd.addSubCommand(new RemoveElementCommand(oldzoom, nextSibling, svgroot)); - - // set new svg document - // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode() - if(svgdoc.adoptNode) { - svgcontent = svgdoc.adoptNode(newDoc.documentElement); - } - else { - svgcontent = svgdoc.importNode(newDoc.documentElement, true); - } - - svgroot.appendChild(svgcontent); - var content = $(svgcontent); - - canvas.current_drawing_ = new svgedit.draw.Drawing(svgcontent, idprefix); - - // retrieve or set the nonce - var nonce = getCurrentDrawing().getNonce(); - if (nonce) { - call("setnonce", nonce); - } else { - call("unsetnonce"); - } - - // change image href vals if possible - content.find('image').each(function() { - var image = this; - preventClickDefault(image); - var val = getHref(this); - if(val.indexOf('data:') === 0) { - // Check if an SVG-edit data URI - var m = val.match(/svgedit_url=(.*?);/); - if(m) { - var url = decodeURIComponent(m[1]); - $(new Image()).load(function() { - image.setAttributeNS(xlinkns,'xlink:href',url); - }).attr('src',url); - } - } - // Add to encodableImages if it loads - canvas.embedImage(val); - }); - - // Wrap child SVGs in group elements - content.find('svg').each(function() { - // Skip if it's in a <defs> - if($(this).closest('defs').length) return; - - uniquifyElems(this); - - // Check if it already has a gsvg group - var pa = this.parentNode; - if(pa.childNodes.length === 1 && pa.nodeName === 'g') { - $(pa).data('gsvg', this); - pa.id = pa.id || getNextId(); - } else { - groupSvgElem(this); - } - }); - - // For Firefox: Put all paint elems in defs - if(svgedit.browser.isGecko()) { - content.find('linearGradient, radialGradient, pattern').appendTo(findDefs()); - } - - - // Set ref element for <use> elements - - // TODO: This should also be done if the object is re-added through "redo" - setUseData(content); - - convertGradients(content[0]); - - // recalculate dimensions on the top-level children so that unnecessary transforms - // are removed - svgedit.utilities.walkTreePost(svgcontent, function(n){try{recalculateDimensions(n)}catch(e){console.log(e)}}); - - var attrs = { - id: 'svgcontent', - overflow: curConfig.show_outside_canvas?'visible':'hidden' - }; - - var percs = false; - - // determine proper size - if (content.attr("viewBox")) { - var vb = content.attr("viewBox").split(' '); - attrs.width = vb[2]; - attrs.height = vb[3]; - } - // handle content that doesn't have a viewBox - else { - $.each(['width', 'height'], function(i, dim) { - // Set to 100 if not given - var val = content.attr(dim); - - if(!val) val = '100%'; - - if((val+'').substr(-1) === "%") { - // Use user units if percentage given - percs = true; - } else { - attrs[dim] = convertToNum(dim, val); - } - }); - } - - // identify layers - identifyLayers(); - - // Give ID for any visible layer children missing one - content.children().find(visElems).each(function() { - if(!this.id) this.id = getNextId(); - }); - - // Percentage width/height, so let's base it on visible elements - if(percs) { - var bb = getStrokedBBox(); - attrs.width = bb.width + bb.x; - attrs.height = bb.height + bb.y; - } - - // Just in case negative numbers are given or - // result from the percs calculation - if(attrs.width <= 0) attrs.width = 100; - if(attrs.height <= 0) attrs.height = 100; - - content.attr(attrs); - this.contentW = attrs['width']; - this.contentH = attrs['height']; - - batchCmd.addSubCommand(new InsertElementCommand(svgcontent)); - // update root to the correct size - var changes = content.attr(["width", "height"]); - batchCmd.addSubCommand(new ChangeElementCommand(svgroot, changes)); - - // reset zoom - current_zoom = 1; - - // reset transform lists - svgedit.transformlist.resetListMap(); - clearSelection(); - svgedit.path.clearData(); - svgroot.appendChild(selectorManager.selectorParentGroup); - - addCommandToHistory(batchCmd); - call("changed", [svgcontent]); - } catch(e) { - console.log(e); - return false; - } - - return true; -}; - -// Function: importSvgString -// This function imports the input SVG XML as a <symbol> in the <defs>, then adds a -// <use> to the current layer. -// -// Parameters: -// xmlString - The SVG as XML text. -// -// Returns: -// This function returns false if the import was unsuccessful, true otherwise. -// TODO: -// * properly handle if namespace is introduced by imported content (must add to svgcontent -// and update all prefixes in the imported node) -// * properly handle recalculating dimensions, recalculateDimensions() doesn't handle -// arbitrary transform lists, but makes some assumptions about how the transform list -// was obtained -// * import should happen in top-left of current zoomed viewport -this.importSvgString = function(xmlString) { - - try { - // Get unique ID - var uid = svgedit.utilities.encode64(xmlString.length + xmlString).substr(0,32); - - var useExisting = false; - - // Look for symbol and make sure symbol exists in image - if(import_ids[uid]) { - if( $(import_ids[uid].symbol).parents('#svgroot').length ) { - useExisting = true; - } - } - - var batchCmd = new BatchCommand("Import SVG"); - - if(useExisting) { - var symbol = import_ids[uid].symbol; - var ts = import_ids[uid].xform; - } else { - // convert string into XML document - var newDoc = svgedit.utilities.text2xml(xmlString); - - this.prepareSvg(newDoc); - - // import new svg document into our document - var svg; - // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode() - if(svgdoc.adoptNode) { - svg = svgdoc.adoptNode(newDoc.documentElement); - } - else { - svg = svgdoc.importNode(newDoc.documentElement, true); - } - - uniquifyElems(svg); - - var innerw = convertToNum('width', svg.getAttribute("width")), - innerh = convertToNum('height', svg.getAttribute("height")), - innervb = svg.getAttribute("viewBox"), - // if no explicit viewbox, create one out of the width and height - vb = innervb ? innervb.split(" ") : [0,0,innerw,innerh]; - for (var j = 0; j < 4; ++j) - vb[j] = +(vb[j]); - - // TODO: properly handle preserveAspectRatio - var canvasw = +svgcontent.getAttribute("width"), - canvash = +svgcontent.getAttribute("height"); - // imported content should be 1/3 of the canvas on its largest dimension - - if (innerh > innerw) { - var ts = "scale(" + (canvash/3)/vb[3] + ")"; - } - else { - var ts = "scale(" + (canvash/3)/vb[2] + ")"; - } - - // Hack to make recalculateDimensions understand how to scale - ts = "translate(0) " + ts + " translate(0)"; - - var symbol = svgdoc.createElementNS(svgns, "symbol"); - var defs = findDefs(); - - if(svgedit.browser.isGecko()) { - // Move all gradients into root for Firefox, workaround for this bug: - // https://bugzilla.mozilla.org/show_bug.cgi?id=353575 - // TODO: Make this properly undo-able. - $(svg).find('linearGradient, radialGradient, pattern').appendTo(defs); - } - - while (svg.firstChild) { - var first = svg.firstChild; - symbol.appendChild(first); - } - var attrs = svg.attributes; - for(var i=0; i < attrs.length; i++) { - var attr = attrs[i]; - symbol.setAttribute(attr.nodeName, attr.nodeValue); - } - symbol.id = getNextId(); - - // Store data - import_ids[uid] = { - symbol: symbol, - xform: ts - } - - findDefs().appendChild(symbol); - batchCmd.addSubCommand(new InsertElementCommand(symbol)); - } - - - var use_el = svgdoc.createElementNS(svgns, "use"); - use_el.id = getNextId(); - setHref(use_el, "#" + symbol.id); - - (current_group || getCurrentDrawing().getCurrentLayer()).appendChild(use_el); - batchCmd.addSubCommand(new InsertElementCommand(use_el)); - clearSelection(); - - use_el.setAttribute("transform", ts); - recalculateDimensions(use_el); - $(use_el).data('symbol', symbol).data('ref', symbol); - addToSelection([use_el]); - - // TODO: Find way to add this in a recalculateDimensions-parsable way -// if (vb[0] != 0 || vb[1] != 0) -// ts = "translate(" + (-vb[0]) + "," + (-vb[1]) + ") " + ts; - addCommandToHistory(batchCmd); - call("changed", [svgcontent]); - - } catch(e) { - console.log(e); - return false; - } - - return true; -}; - -// TODO(codedread): Move all layer/context functions in draw.js -// Layer API Functions - -// Group: Layers - -// Function: identifyLayers -// Updates layer system -var identifyLayers = canvas.identifyLayers = function() { - leaveContext(); - getCurrentDrawing().identifyLayers(); -}; - -// Function: createLayer -// Creates a new top-level layer in the drawing with the given name, sets the current layer -// to it, and then clears the selection This function then calls the 'changed' handler. -// This is an undoable action. -// -// Parameters: -// name - The given name -this.createLayer = function(name) { - var batchCmd = new BatchCommand("Create Layer"); - var new_layer = getCurrentDrawing().createLayer(name); - batchCmd.addSubCommand(new InsertElementCommand(new_layer)); - addCommandToHistory(batchCmd); - clearSelection(); - call("changed", [new_layer]); -}; - -// Function: cloneLayer -// Creates a new top-level layer in the drawing with the given name, copies all the current layer's contents -// to it, and then clears the selection This function then calls the 'changed' handler. -// This is an undoable action. -// -// Parameters: -// name - The given name -this.cloneLayer = function(name) { - var batchCmd = new BatchCommand("Duplicate Layer"); - var new_layer = svgdoc.createElementNS(svgns, "g"); - var layer_title = svgdoc.createElementNS(svgns, "title"); - layer_title.textContent = name; - new_layer.appendChild(layer_title); - var current_layer = getCurrentDrawing().getCurrentLayer(); - $(current_layer).after(new_layer); - var childs = current_layer.childNodes; - for(var i = 0; i < childs.length; i++) { - var ch = childs[i]; - if(ch.localName == 'title') continue; - new_layer.appendChild(copyElem(ch)); - } - - clearSelection(); - identifyLayers(); - - batchCmd.addSubCommand(new InsertElementCommand(new_layer)); - addCommandToHistory(batchCmd); - canvas.setCurrentLayer(name); - call("changed", [new_layer]); -}; - -// Function: deleteCurrentLayer -// Deletes the current layer from the drawing and then clears the selection. This function -// then calls the 'changed' handler. This is an undoable action. -this.deleteCurrentLayer = function() { - var current_layer = getCurrentDrawing().getCurrentLayer(); - var nextSibling = current_layer.nextSibling; - var parent = current_layer.parentNode; - current_layer = getCurrentDrawing().deleteCurrentLayer(); - if (current_layer) { - var batchCmd = new BatchCommand("Delete Layer"); - // store in our Undo History - batchCmd.addSubCommand(new RemoveElementCommand(current_layer, nextSibling, parent)); - addCommandToHistory(batchCmd); - clearSelection(); - call("changed", [parent]); - return true; - } - return false; -}; - -// Function: setCurrentLayer -// Sets the current layer. If the name is not a valid layer name, then this function returns -// false. Otherwise it returns true. This is not an undo-able action. -// -// Parameters: -// name - the name of the layer you want to switch to. -// -// Returns: -// true if the current layer was switched, otherwise false -this.setCurrentLayer = function(name) { - var result = getCurrentDrawing().setCurrentLayer(svgedit.utilities.toXml(name)); - if (result) { - clearSelection(); - } - return result; -}; - -// Function: renameCurrentLayer -// Renames the current layer. If the layer name is not valid (i.e. unique), then this function -// does nothing and returns false, otherwise it returns true. This is an undo-able action. -// -// Parameters: -// newname - the new name you want to give the current layer. This name must be unique -// among all layer names. -// -// Returns: -// true if the rename succeeded, false otherwise. -this.renameCurrentLayer = function(newname) { - var drawing = getCurrentDrawing(); - if (drawing.current_layer) { - var oldLayer = drawing.current_layer; - // setCurrentLayer will return false if the name doesn't already exist - // this means we are free to rename our oldLayer - if (!canvas.setCurrentLayer(newname)) { - var batchCmd = new BatchCommand("Rename Layer"); - // find the index of the layer - for (var i = 0; i < drawing.getNumLayers(); ++i) { - if (drawing.all_layers[i][1] == oldLayer) break; - } - var oldname = drawing.getLayerName(i); - drawing.all_layers[i][0] = svgedit.utilities.toXml(newname); - - // now change the underlying title element contents - var len = oldLayer.childNodes.length; - for (var i = 0; i < len; ++i) { - var child = oldLayer.childNodes.item(i); - // found the <title> element, now append all the - if (child && child.tagName == "title") { - // wipe out old name - while (child.firstChild) { child.removeChild(child.firstChild); } - child.textContent = newname; - - batchCmd.addSubCommand(new ChangeElementCommand(child, {"#text":oldname})); - addCommandToHistory(batchCmd); - call("changed", [oldLayer]); - return true; - } - } - } - drawing.current_layer = oldLayer; - } - return false; -}; - -// Function: setCurrentLayerPosition -// Changes the position of the current layer to the new value. If the new index is not valid, -// this function does nothing and returns false, otherwise it returns true. This is an -// undo-able action. -// -// Parameters: -// newpos - The zero-based index of the new position of the layer. This should be between -// 0 and (number of layers - 1) -// -// Returns: -// true if the current layer position was changed, false otherwise. -this.setCurrentLayerPosition = function(newpos) { - var drawing = getCurrentDrawing(); - if (drawing.current_layer && newpos >= 0 && newpos < drawing.getNumLayers()) { - for (var oldpos = 0; oldpos < drawing.getNumLayers(); ++oldpos) { - if (drawing.all_layers[oldpos][1] == drawing.current_layer) break; - } - // some unknown error condition (current_layer not in all_layers) - if (oldpos == drawing.getNumLayers()) { return false; } - - if (oldpos != newpos) { - // if our new position is below us, we need to insert before the node after newpos - var refLayer = null; - var oldNextSibling = drawing.current_layer.nextSibling; - if (newpos > oldpos ) { - if (newpos < drawing.getNumLayers()-1) { - refLayer = drawing.all_layers[newpos+1][1]; - } - } - // if our new position is above us, we need to insert before the node at newpos - else { - refLayer = drawing.all_layers[newpos][1]; - } - svgcontent.insertBefore(drawing.current_layer, refLayer); - addCommandToHistory(new MoveElementCommand(drawing.current_layer, oldNextSibling, svgcontent)); - - identifyLayers(); - canvas.setCurrentLayer(drawing.getLayerName(newpos)); - - return true; - } - } - - return false; -}; - -// Function: setLayerVisibility -// Sets the visibility of the layer. If the layer name is not valid, this function return -// false, otherwise it returns true. This is an undo-able action. -// -// Parameters: -// layername - the name of the layer to change the visibility -// bVisible - true/false, whether the layer should be visible -// -// Returns: -// true if the layer's visibility was set, false otherwise -this.setLayerVisibility = function(layername, bVisible) { - var drawing = getCurrentDrawing(); - var prevVisibility = drawing.getLayerVisibility(layername); - var layer = drawing.setLayerVisibility(layername, bVisible); - if (layer) { - var oldDisplay = prevVisibility ? 'inline' : 'none'; - addCommandToHistory(new ChangeElementCommand(layer, {'display':oldDisplay}, 'Layer Visibility')); - } else { - return false; - } - - if (layer == drawing.getCurrentLayer()) { - clearSelection(); - pathActions.clear(); - } -// call("changed", [selected]); - return true; -}; - -// Function: moveSelectedToLayer -// Moves the selected elements to layername. If the name is not a valid layer name, then false -// is returned. Otherwise it returns true. This is an undo-able action. -// -// Parameters: -// layername - the name of the layer you want to which you want to move the selected elements -// -// Returns: -// true if the selected elements were moved to the layer, false otherwise. -this.moveSelectedToLayer = function(layername) { - // find the layer - var layer = null; - var drawing = getCurrentDrawing(); - for (var i = 0; i < drawing.getNumLayers(); ++i) { - if (drawing.getLayerName(i) == layername) { - layer = drawing.all_layers[i][1]; - break; - } - } - if (!layer) return false; - - var batchCmd = new BatchCommand("Move Elements to Layer"); - - // loop for each selected element and move it - var selElems = selectedElements; - var i = selElems.length; - while (i--) { - var elem = selElems[i]; - if (!elem) continue; - var oldNextSibling = elem.nextSibling; - // TODO: this is pretty brittle! - var oldLayer = elem.parentNode; - layer.appendChild(elem); - batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldLayer)); - } - - addCommandToHistory(batchCmd); - - return true; -}; - -this.mergeLayer = function(skipHistory) { - var batchCmd = new BatchCommand("Merge Layer"); - var drawing = getCurrentDrawing(); - var prev = $(drawing.current_layer).prev()[0]; - if(!prev) return; - var childs = drawing.current_layer.childNodes; - var len = childs.length; - var layerNextSibling = drawing.current_layer.nextSibling; - batchCmd.addSubCommand(new RemoveElementCommand(drawing.current_layer, layerNextSibling, svgcontent)); - - while(drawing.current_layer.firstChild) { - var ch = drawing.current_layer.firstChild; - if(ch.localName == 'title') { - var chNextSibling = ch.nextSibling; - batchCmd.addSubCommand(new RemoveElementCommand(ch, chNextSibling, drawing.current_layer)); - drawing.current_layer.removeChild(ch); - continue; - } - var oldNextSibling = ch.nextSibling; - prev.appendChild(ch); - batchCmd.addSubCommand(new MoveElementCommand(ch, oldNextSibling, drawing.current_layer)); - } - - // Remove current layer - svgcontent.removeChild(drawing.current_layer); - - if(!skipHistory) { - clearSelection(); - identifyLayers(); - - call("changed", [svgcontent]); - - addCommandToHistory(batchCmd); - } - - drawing.current_layer = prev; - return batchCmd; -} - -this.mergeAllLayers = function() { - var batchCmd = new BatchCommand("Merge all Layers"); - var drawing = getCurrentDrawing(); - drawing.current_layer = drawing.all_layers[drawing.getNumLayers()-1][1]; - while($(svgcontent).children('g').length > 1) { - batchCmd.addSubCommand(canvas.mergeLayer(true)); - } - - clearSelection(); - identifyLayers(); - call("changed", [svgcontent]); - addCommandToHistory(batchCmd); -} - -// Function: leaveContext -// Return from a group context to the regular kind, make any previously -// disabled elements enabled again -var leaveContext = this.leaveContext = function() { - var len = disabled_elems.length; - if(len) { - for(var i = 0; i < len; i++) { - var elem = disabled_elems[i]; - - var orig = elData(elem, 'orig_opac'); - if(orig !== 1) { - elem.setAttribute('opacity', orig); - } else { - elem.removeAttribute('opacity'); - } - elem.setAttribute('style', 'pointer-events: inherit'); - } - disabled_elems = []; - clearSelection(true); - call("contextset", null); - } - current_group = null; -} - -// Function: setContext -// Set the current context (for in-group editing) -var setContext = this.setContext = function(elem) { - leaveContext(); - if(typeof elem === 'string') { - elem = getElem(elem); - } - - // Edit inside this group - current_group = elem; - - // Disable other elements - $(elem).parentsUntil('#svgcontent').andSelf().siblings().each(function() { - var opac = this.getAttribute('opacity') || 1; - // Store the original's opacity - elData(this, 'orig_opac', opac); - this.setAttribute('opacity', opac * .33); - this.setAttribute('style', 'pointer-events: none'); - disabled_elems.push(this); - }); - - clearSelection(); - call("contextset", current_group); -} - -// Group: Document functions - -// Function: clear -// Clears the current document. This is not an undoable action. -this.clear = function() { - pathActions.clear(); - - clearSelection(); - - // clear the svgcontent node - canvas.clearSvgContentElement(); - - // create new document - canvas.current_drawing_ = new svgedit.draw.Drawing(svgcontent); - - // create default layers - canvas.createLayer("Labels"); - canvas.createLayer("Masks"); - - // clear the undo stack - canvas.undoMgr.resetUndoStack(); - - // reset the selector manager - selectorManager.initGroup(); - - // reset the rubber band box - rubberBox = selectorManager.getRubberBandBox(); - - call("cleared"); -}; - -// Function: linkControlPoints -// Alias function -this.linkControlPoints = pathActions.linkControlPoints; - -// Function: getContentElem -// Returns the content DOM element -this.getContentElem = function() { return svgcontent; }; - -// Function: getRootElem -// Returns the root DOM element -this.getRootElem = function() { return svgroot; }; - -// Function: getSelectedElems -// Returns the array with selected DOM elements -this.getSelectedElems = function() { return selectedElements; }; - -// Function: getResolution -// Returns the current dimensions and zoom level in an object -var getResolution = this.getResolution = function() { -// var vb = svgcontent.getAttribute("viewBox").split(' '); -// return {'w':vb[2], 'h':vb[3], 'zoom': current_zoom}; - - var width = svgcontent.getAttribute("width")/current_zoom; - var height = svgcontent.getAttribute("height")/current_zoom; - - return { - 'w': width, - 'h': height, - 'zoom': current_zoom - }; -}; - -// Function: getZoom -// Returns the current zoom level -this.getZoom = function(){return current_zoom;}; - -// Function: getVersion -// Returns a string which describes the revision number of SvgCanvas. -this.getVersion = function() { - return "svgcanvas.js ($Rev: 2199 $)"; -}; - -// Function: setUiStrings -// Update interface strings with given values -// -// Parameters: -// strs - Object with strings (see uiStrings for examples) -this.setUiStrings = function(strs) { - $.extend(uiStrings, strs.notification); -} - -// Function: setConfig -// Update configuration options with given values -// -// Parameters: -// opts - Object with options (see curConfig for examples) -this.setConfig = function(opts) { - $.extend(curConfig, opts); -} - -// Function: getTitle -// Returns the current group/SVG's title contents -this.getTitle = function(elem) { - elem = elem || selectedElements[0]; - if(!elem) return; - elem = $(elem).data('gsvg') || $(elem).data('symbol') || elem; - var childs = elem.childNodes; - for (var i=0; i<childs.length; i++) { - if(childs[i].nodeName == 'title') { - return childs[i].textContent; - } - } - return ''; -} - -// Function: setGroupTitle -// Sets the group/SVG's title content -// TODO: Combine this with setDocumentTitle -this.setGroupTitle = function(val) { - var elem = selectedElements[0]; - elem = $(elem).data('gsvg') || elem; - - var ts = $(elem).children('title'); - - var batchCmd = new BatchCommand("Set Label"); - - if(!val.length) { - // Remove title element - var tsNextSibling = ts.nextSibling; - batchCmd.addSubCommand(new RemoveElementCommand(ts[0], tsNextSibling, elem)); - ts.remove(); - } else if(ts.length) { - // Change title contents - var title = ts[0]; - batchCmd.addSubCommand(new ChangeElementCommand(title, {'#text': title.textContent})); - title.textContent = val; - } else { - // Add title element - title = svgdoc.createElementNS(svgns, "title"); - title.textContent = val; - $(elem).prepend(title); - batchCmd.addSubCommand(new InsertElementCommand(title)); - } - - addCommandToHistory(batchCmd); -} - -// Function: getDocumentTitle -// Returns the current document title or an empty string if not found -this.getDocumentTitle = function() { - return canvas.getTitle(svgcontent); -} - -// Function: setDocumentTitle -// Adds/updates a title element for the document with the given name. -// This is an undoable action -// -// Parameters: -// newtitle - String with the new title -this.setDocumentTitle = function(newtitle) { - var childs = svgcontent.childNodes, doc_title = false, old_title = ''; - - var batchCmd = new BatchCommand("Change Image Title"); - - for (var i=0; i<childs.length; i++) { - if(childs[i].nodeName == 'title') { - doc_title = childs[i]; - old_title = doc_title.textContent; - break; - } - } - if(!doc_title) { - doc_title = svgdoc.createElementNS(svgns, "title"); - svgcontent.insertBefore(doc_title, svgcontent.firstChild); - } - - if(newtitle.length) { - doc_title.textContent = newtitle; - } else { - // No title given, so element is not necessary - doc_title.parentNode.removeChild(doc_title); - } - batchCmd.addSubCommand(new ChangeElementCommand(doc_title, {'#text': old_title})); - addCommandToHistory(batchCmd); -} - -// Function: getEditorNS -// Returns the editor's namespace URL, optionally adds it to root element -// -// Parameters: -// add - Boolean to indicate whether or not to add the namespace value -this.getEditorNS = function(add) { - if(add) { - svgcontent.setAttribute('xmlns:se', se_ns); - } - return se_ns; -} - -// Function: setResolution -// Changes the document's dimensions to the given size -// -// Parameters: -// x - Number with the width of the new dimensions in user units. -// Can also be the string "fit" to indicate "fit to content" -// y - Number with the height of the new dimensions in user units. -// -// Returns: -// Boolean to indicate if resolution change was succesful. -// It will fail on "fit to content" option with no content to fit to. -this.setResolution = function(x, y) { - var res = getResolution(); - var w = res.w, h = res.h; - var batchCmd; - - if(x == 'fit') { - // Get bounding box - var bbox = getStrokedBBox(); - - if(bbox) { - batchCmd = new BatchCommand("Fit Canvas to Content"); - var visEls = getVisibleElements(); - addToSelection(visEls); - var dx = [], dy = []; - $.each(visEls, function(i, item) { - dx.push(bbox.x*-1); - dy.push(bbox.y*-1); - }); - - var cmd = canvas.moveSelectedElements(dx, dy, true); - batchCmd.addSubCommand(cmd); - clearSelection(); - - x = Math.round(bbox.width); - y = Math.round(bbox.height); - } else { - return false; - } - } - if (x != w || y != h) { - var handle = svgroot.suspendRedraw(1000); - if(!batchCmd) { - batchCmd = new BatchCommand("Change Image Dimensions"); - } - - x = convertToNum('width', x); - y = convertToNum('height', y); - - svgcontent.setAttribute('width', x); - svgcontent.setAttribute('height', y); - - this.contentW = x; - this.contentH = y; - batchCmd.addSubCommand(new ChangeElementCommand(svgcontent, {"width":w, "height":h})); - - svgcontent.setAttribute("viewBox", [0, 0, x/current_zoom, y/current_zoom].join(' ')); - batchCmd.addSubCommand(new ChangeElementCommand(svgcontent, {"viewBox": ["0 0", w, h].join(' ')})); - - addCommandToHistory(batchCmd); - svgroot.unsuspendRedraw(handle); - call("changed", [svgcontent]); - } - return true; -}; - -// Function: getOffset -// Returns an object with x, y values indicating the svgcontent element's -// position in the editor's canvas. -this.getOffset = function() { - return $(svgcontent).attr(['x', 'y']); -} - -// Function: setBBoxZoom -// Sets the zoom level on the canvas-side based on the given value -// -// Parameters: -// val - Bounding box object to zoom to or string indicating zoom option -// editor_w - Integer with the editor's workarea box's width -// editor_h - Integer with the editor's workarea box's height -this.setBBoxZoom = function(val, editor_w, editor_h) { - var spacer = .85; - var bb; - var calcZoom = function(bb) { - if(!bb) return false; - var w_zoom = Math.round((editor_w / bb.width)*100 * spacer)/100; - var h_zoom = Math.round((editor_h / bb.height)*100 * spacer)/100; - var zoomlevel = Math.min(w_zoom,h_zoom); - canvas.setZoom(zoomlevel); - return {'zoom': zoomlevel, 'bbox': bb}; - } - - if(typeof val == 'object') { - bb = val; - if(bb.width == 0 || bb.height == 0) { - var newzoom = bb.zoom?bb.zoom:current_zoom * bb.factor; - canvas.setZoom(newzoom); - return {'zoom': current_zoom, 'bbox': bb}; - } - return calcZoom(bb); - } - - switch (val) { - case 'selection': - if(!selectedElements[0]) return; - var sel_elems = $.map(selectedElements, function(n){ if(n) return n; }); - bb = getStrokedBBox(sel_elems); - break; - case 'canvas': - var res = getResolution(); - spacer = .97; - bb = {width:res.w, height:res.h , x:4, y:4}; - break; - case 'content': - bb = getStrokedBBox(); - break; - case 'layer': - bb = getStrokedBBox(getVisibleElements(getCurrentDrawing().getCurrentLayer())); - break; - default: - return; - } - return calcZoom(bb); -} - -// Function: setZoom -// Sets the zoom to the given level -// -// Parameters: -// zoomlevel - Float indicating the zoom level to change to -this.setZoom = function(zoomlevel) { - var res = getResolution(); - svgcontent.setAttribute("viewBox", "0 0 " + res.w/zoomlevel + " " + res.h/zoomlevel); - current_zoom = zoomlevel; - $.each(selectedElements, function(i, elem) { - if(!elem) return; - selectorManager.requestSelector(elem).resize(); - }); - pathActions.zoomChange(); - runExtensions("zoomChanged", zoomlevel); -} - -// Function: getMode -// Returns the current editor mode string -this.getMode = function() { - return current_mode; -}; - -// Function: setMode -// Sets the editor's mode to the given string -// -// Parameters: -// name - String with the new mode to change to -this.setMode = function(name) { - pathActions.clear(true); - textActions.clear(); - cur_properties = (selectedElements[0] && selectedElements[0].nodeName == 'text') ? cur_text : cur_shape; - current_mode = name; -}; - -// Group: Element Styling - -// Function: getColor -// Returns the current fill/stroke option -this.getColor = function(type) { - return cur_properties[type]; -}; - -// Function: setColor -// Change the current stroke/fill color/gradient value -// -// Parameters: -// type - String indicating fill or stroke -// val - The value to set the stroke attribute to -// preventUndo - Boolean indicating whether or not this should be and undoable option -this.setColor = function(type, val, preventUndo) { - cur_shape[type] = val; - cur_properties[type + '_paint'] = {type:"solidColor"}; - var elems = []; - var i = selectedElements.length; - while (i--) { - var elem = selectedElements[i]; - if (elem) { - if (elem.tagName == "g") - svgedit.utilities.walkTree(elem, function(e){if(e.nodeName!="g") elems.push(e);}); - else { - if(type == 'fill') { - if(elem.tagName != "polyline" && elem.tagName != "line") { - elems.push(elem); - } - } else { - elems.push(elem); - } - } - } - } - if (elems.length > 0) { - if (!preventUndo) { - changeSelectedAttribute(type, val, elems); - call("changed", elems); - } else - changeSelectedAttributeNoUndo(type, val, elems); - } -} - - -// Function: findDefs -// Return the document's <defs> element, create it first if necessary -var findDefs = function() { - var defs = svgcontent.getElementsByTagNameNS(svgns, "defs"); - if (defs.length > 0) { - defs = defs[0]; - } - else { - defs = svgdoc.createElementNS(svgns, "defs" ); - if(svgcontent.firstChild) { - // first child is a comment, so call nextSibling - svgcontent.insertBefore( defs, svgcontent.firstChild.nextSibling); - } else { - svgcontent.appendChild(defs); - } - } - return defs; -}; - -// Function: setGradient -// Apply the current gradient to selected element's fill or stroke -// -// Parameters -// type - String indicating "fill" or "stroke" to apply to an element -var setGradient = this.setGradient = function(type) { - if(!cur_properties[type + '_paint'] || cur_properties[type + '_paint'].type == "solidColor") return; - var grad = canvas[type + 'Grad']; - // find out if there is a duplicate gradient already in the defs - var duplicate_grad = findDuplicateGradient(grad); - var defs = findDefs(); - // no duplicate found, so import gradient into defs - if (!duplicate_grad) { - var orig_grad = grad; - grad = defs.appendChild( svgdoc.importNode(grad, true) ); - // get next id and set it on the grad - grad.id = getNextId(); - } - else { // use existing gradient - grad = duplicate_grad; - } - canvas.setColor(type, "url(#" + grad.id + ")"); -} - -// Function: findDuplicateGradient -// Check if exact gradient already exists -// -// Parameters: -// grad - The gradient DOM element to compare to others -// -// Returns: -// The existing gradient if found, null if not -var findDuplicateGradient = function(grad) { - var defs = findDefs(); - var existing_grads = $(defs).find("linearGradient, radialGradient"); - var i = existing_grads.length; - var rad_attrs = ['r','cx','cy','fx','fy']; - while (i--) { - var og = existing_grads[i]; - if(grad.tagName == "linearGradient") { - if (grad.getAttribute('x1') != og.getAttribute('x1') || - grad.getAttribute('y1') != og.getAttribute('y1') || - grad.getAttribute('x2') != og.getAttribute('x2') || - grad.getAttribute('y2') != og.getAttribute('y2')) - { - continue; - } - } else { - var grad_attrs = $(grad).attr(rad_attrs); - var og_attrs = $(og).attr(rad_attrs); - - var diff = false; - $.each(rad_attrs, function(i, attr) { - if(grad_attrs[attr] != og_attrs[attr]) diff = true; - }); - - if(diff) continue; - } - - // else could be a duplicate, iterate through stops - var stops = grad.getElementsByTagNameNS(svgns, "stop"); - var ostops = og.getElementsByTagNameNS(svgns, "stop"); - - if (stops.length != ostops.length) { - continue; - } - - var j = stops.length; - while(j--) { - var stop = stops[j]; - var ostop = ostops[j]; - - if (stop.getAttribute('offset') != ostop.getAttribute('offset') || - stop.getAttribute('stop-opacity') != ostop.getAttribute('stop-opacity') || - stop.getAttribute('stop-color') != ostop.getAttribute('stop-color')) - { - break; - } - } - - if (j == -1) { - return og; - } - } // for each gradient in defs - - return null; -}; - -function reorientGrads(elem, m) { - var bb = svgedit.utilities.getBBox(elem); - for(var i = 0; i < 2; i++) { - var type = i === 0 ? 'fill' : 'stroke'; - var attrVal = elem.getAttribute(type); - if(attrVal && attrVal.indexOf('url(') === 0) { - var grad = getRefElem(attrVal); - if(grad.tagName === 'linearGradient') { - var x1 = grad.getAttribute('x1') || 0; - var y1 = grad.getAttribute('y1') || 0; - var x2 = grad.getAttribute('x2') || 1; - var y2 = grad.getAttribute('y2') || 0; - - // Convert to USOU points - x1 = (bb.width * x1) + bb.x; - y1 = (bb.height * y1) + bb.y; - x2 = (bb.width * x2) + bb.x; - y2 = (bb.height * y2) + bb.y; - - // Transform those points - var pt1 = transformPoint(x1, y1, m); - var pt2 = transformPoint(x2, y2, m); - - // Convert back to BB points - var g_coords = {}; - - g_coords.x1 = (pt1.x - bb.x) / bb.width; - g_coords.y1 = (pt1.y - bb.y) / bb.height; - g_coords.x2 = (pt2.x - bb.x) / bb.width; - g_coords.y2 = (pt2.y - bb.y) / bb.height; - - var newgrad = grad.cloneNode(true); - $(newgrad).attr(g_coords); - - newgrad.id = getNextId(); - findDefs().appendChild(newgrad); - elem.setAttribute(type, 'url(#' + newgrad.id + ')'); - } - } - } -} - -// Function: setPaint -// Set a color/gradient to a fill/stroke -// -// Parameters: -// type - String with "fill" or "stroke" -// paint - The jGraduate paint object to apply -this.setPaint = function(type, paint) { - // make a copy - var p = new $.jGraduate.Paint(paint); - this.setPaintOpacity(type, p.alpha/100, true); - - // now set the current paint object - cur_properties[type + '_paint'] = p; - switch ( p.type ) { - case "solidColor": - this.setColor(type, p.solidColor != "none" ? "#"+p.solidColor : "none");; - break; - case "linearGradient": - case "radialGradient": - canvas[type + 'Grad'] = p[p.type]; - setGradient(type); - break; - default: -// console.log("none!"); - } -}; - - -// this.setStrokePaint = function(p) { -// // make a copy -// var p = new $.jGraduate.Paint(p); -// this.setStrokeOpacity(p.alpha/100); -// -// // now set the current paint object -// cur_properties.stroke_paint = p; -// switch ( p.type ) { -// case "solidColor": -// this.setColor('stroke', p.solidColor != "none" ? "#"+p.solidColor : "none");; -// break; -// case "linearGradient" -// case "radialGradient" -// canvas.strokeGrad = p[p.type]; -// setGradient(type); -// default: -// // console.log("none!"); -// } -// }; -// -// this.setFillPaint = function(p, addGrad) { -// // make a copy -// var p = new $.jGraduate.Paint(p); -// this.setFillOpacity(p.alpha/100, true); -// -// // now set the current paint object -// cur_properties.fill_paint = p; -// if (p.type == "solidColor") { -// this.setColor('fill', p.solidColor != "none" ? "#"+p.solidColor : "none"); -// } -// else if(p.type == "linearGradient") { -// canvas.fillGrad = p.linearGradient; -// if(addGrad) setGradient(); -// } -// else if(p.type == "radialGradient") { -// canvas.fillGrad = p.radialGradient; -// if(addGrad) setGradient(); -// } -// else { -// // console.log("none!"); -// } -// }; - -// Function: getStrokeWidth -// Returns the current stroke-width value -this.getStrokeWidth = function() { - return cur_properties.stroke_width; -}; - -// Function: setStrokeWidth -// Sets the stroke width for the current selected elements -// When attempting to set a line's width to 0, this changes it to 1 instead -// -// Parameters: -// val - A Float indicating the new stroke width value -this.setStrokeWidth = function(val) { - if(val == 0 && ['line', 'path'].indexOf(current_mode) >= 0) { - canvas.setStrokeWidth(1); - return; - } - cur_properties.stroke_width = val; - - var elems = []; - var i = selectedElements.length; - while (i--) { - var elem = selectedElements[i]; - if (elem) { - if (elem.tagName == "g") - svgedit.utilities.walkTree(elem, function(e){if(e.nodeName!="g") elems.push(e);}); - else - elems.push(elem); - } - } - if (elems.length > 0) { - changeSelectedAttribute("stroke-width", val, elems); - call("changed", selectedElements); - } -}; - -// Function: setStrokeAttr -// Set the given stroke-related attribute the given value for selected elements -// -// Parameters: -// attr - String with the attribute name -// val - String or number with the attribute value -this.setStrokeAttr = function(attr, val) { - cur_shape[attr.replace('-','_')] = val; - var elems = []; - var i = selectedElements.length; - while (i--) { - var elem = selectedElements[i]; - if (elem) { - if (elem.tagName == "g") - svgedit.utilities.walkTree(elem, function(e){if(e.nodeName!="g") elems.push(e);}); - else - elems.push(elem); - } - } - if (elems.length > 0) { - changeSelectedAttribute(attr, val, elems); - call("changed", selectedElements); - } -}; - -// Function: getStyle -// Returns current style options -this.getStyle = function() { - return cur_shape; -} - -// Function: getOpacity -// Returns the current opacity -this.getOpacity = function() { - return cur_shape.opacity; -}; - -// Function: setOpacity -// Sets the given opacity to the current selected elements -this.setOpacity = function(val) { - cur_shape.opacity = val; - changeSelectedAttribute("opacity", val); -}; - -// Function: getOpacity -// Returns the current fill opacity -this.getFillOpacity = function() { - return cur_shape.fill_opacity; -}; - -// Function: getStrokeOpacity -// Returns the current stroke opacity -this.getStrokeOpacity = function() { - return cur_shape.stroke_opacity; -}; - -// Function: setPaintOpacity -// Sets the current fill/stroke opacity -// -// Parameters: -// type - String with "fill" or "stroke" -// val - Float with the new opacity value -// preventUndo - Boolean indicating whether or not this should be an undoable action -this.setPaintOpacity = function(type, val, preventUndo) { - cur_shape[type + '_opacity'] = val; - if (!preventUndo) - changeSelectedAttribute(type + "-opacity", val); - else - changeSelectedAttributeNoUndo(type + "-opacity", val); -}; - -// Function: getBlur -// Gets the stdDeviation blur value of the given element -// -// Parameters: -// elem - The element to check the blur value for -this.getBlur = function(elem) { - var val = 0; -// var elem = selectedElements[0]; - - if(elem) { - var filter_url = elem.getAttribute('filter'); - if(filter_url) { - var blur = getElem(elem.id + '_blur'); - if(blur) { - val = blur.firstChild.getAttribute('stdDeviation'); - } - } - } - return val; -}; - -(function() { - var cur_command = null; - var filter = null; - var filterHidden = false; - - // Function: setBlurNoUndo - // Sets the stdDeviation blur value on the selected element without being undoable - // - // Parameters: - // val - The new stdDeviation value - canvas.setBlurNoUndo = function(val) { - if(!filter) { - canvas.setBlur(val); - return; - } - if(val === 0) { - // Don't change the StdDev, as that will hide the element. - // Instead, just remove the value for "filter" - changeSelectedAttributeNoUndo("filter", ""); - filterHidden = true; - } else { - var elem = selectedElements[0]; - if(filterHidden) { - changeSelectedAttributeNoUndo("filter", 'url(#' + elem.id + '_blur)'); - } - if(svgedit.browser.isWebkit()) { - console.log('e', elem); - elem.removeAttribute('filter'); - elem.setAttribute('filter', 'url(#' + elem.id + '_blur)'); - } - changeSelectedAttributeNoUndo("stdDeviation", val, [filter.firstChild]); - canvas.setBlurOffsets(filter, val); - } - } - - function finishChange() { - var bCmd = canvas.undoMgr.finishUndoableChange(); - cur_command.addSubCommand(bCmd); - addCommandToHistory(cur_command); - cur_command = null; - filter = null; - } - - // Function: setBlurOffsets - // Sets the x, y, with, height values of the filter element in order to - // make the blur not be clipped. Removes them if not neeeded - // - // Parameters: - // filter - The filter DOM element to update - // stdDev - The standard deviation value on which to base the offset size - canvas.setBlurOffsets = function(filter, stdDev) { - if(stdDev > 3) { - // TODO: Create algorithm here where size is based on expected blur - assignAttributes(filter, { - x: '-50%', - y: '-50%', - width: '200%', - height: '200%' - }, 100); - } else { - // Removing these attributes hides text in Chrome (see Issue 579) - if(!svgedit.browser.isWebkit()) { - filter.removeAttribute('x'); - filter.removeAttribute('y'); - filter.removeAttribute('width'); - filter.removeAttribute('height'); - } - } - } - - // Function: setBlur - // Adds/updates the blur filter to the selected element - // - // Parameters: - // val - Float with the new stdDeviation blur value - // complete - Boolean indicating whether or not the action should be completed (to add to the undo manager) - canvas.setBlur = function(val, complete) { - if(cur_command) { - finishChange(); - return; - } - - // Looks for associated blur, creates one if not found - var elem = selectedElements[0]; - var elem_id = elem.id; - filter = getElem(elem_id + '_blur'); - - val -= 0; - - var batchCmd = new BatchCommand(); - - // Blur found! - if(filter) { - if(val === 0) { - filter = null; - } - } else { - // Not found, so create - var newblur = addSvgElementFromJson({ "element": "feGaussianBlur", - "attr": { - "in": 'SourceGraphic', - "stdDeviation": val - } - }); - - filter = addSvgElementFromJson({ "element": "filter", - "attr": { - "id": elem_id + '_blur' - } - }); - - filter.appendChild(newblur); - findDefs().appendChild(filter); - - batchCmd.addSubCommand(new InsertElementCommand(filter)); - } - - var changes = {filter: elem.getAttribute('filter')}; - - if(val === 0) { - elem.removeAttribute("filter"); - batchCmd.addSubCommand(new ChangeElementCommand(elem, changes)); - return; - } else { - changeSelectedAttribute("filter", 'url(#' + elem_id + '_blur)'); - - batchCmd.addSubCommand(new ChangeElementCommand(elem, changes)); - - canvas.setBlurOffsets(filter, val); - } - - cur_command = batchCmd; - canvas.undoMgr.beginUndoableChange("stdDeviation", [filter?filter.firstChild:null]); - if(complete) { - canvas.setBlurNoUndo(val); - finishChange(); - } - }; -}()); - -// Function: getBold -// Check whether selected element is bold or not -// -// Returns: -// Boolean indicating whether or not element is bold -this.getBold = function() { - // should only have one element selected - var selected = selectedElements[0]; - if (selected != null && selected.tagName == "text" && - selectedElements[1] == null) - { - return (selected.getAttribute("font-weight") == "bold"); - } - return false; -}; - -// Function: setBold -// Make the selected element bold or normal -// -// Parameters: -// b - Boolean indicating bold (true) or normal (false) -this.setBold = function(b) { - var selected = selectedElements[0]; - if (selected != null && selected.tagName == "text" && - selectedElements[1] == null) - { - changeSelectedAttribute("font-weight", b ? "bold" : "normal"); - } - if(!selectedElements[0].textContent) { - textActions.setCursor(); - } -}; - -// Function: getItalic -// Check whether selected element is italic or not -// -// Returns: -// Boolean indicating whether or not element is italic -this.getItalic = function() { - var selected = selectedElements[0]; - if (selected != null && selected.tagName == "text" && - selectedElements[1] == null) - { - return (selected.getAttribute("font-style") == "italic"); - } - return false; -}; - -// Function: setItalic -// Make the selected element italic or normal -// -// Parameters: -// b - Boolean indicating italic (true) or normal (false) -this.setItalic = function(i) { - var selected = selectedElements[0]; - if (selected != null && selected.tagName == "text" && - selectedElements[1] == null) - { - changeSelectedAttribute("font-style", i ? "italic" : "normal"); - } - if(!selectedElements[0].textContent) { - textActions.setCursor(); - } -}; - -// Function: getFontFamily -// Returns the current font family -this.getFontFamily = function() { - return cur_text.font_family; -}; - -// Function: setFontFamily -// Set the new font family -// -// Parameters: -// val - String with the new font family -this.setFontFamily = function(val) { - cur_text.font_family = val; - changeSelectedAttribute("font-family", val); - if(selectedElements[0] && !selectedElements[0].textContent) { - textActions.setCursor(); - } -}; - - -// Function: setFontColor -// Set the new font color -// -// Parameters: -// val - String with the new font color -this.setFontColor = function(val) { - cur_text.fill = val; - changeSelectedAttribute("fill", val); -}; - -// Function: getFontColor -// Returns the current font color -this.getFontSize = function() { - return cur_text.fill; -}; - -// Function: getFontSize -// Returns the current font size -this.getFontSize = function() { - return cur_text.font_size; -}; - -// Function: setFontSize -// Applies the given font size to the selected element -// -// Parameters: -// val - Float with the new font size -this.setFontSize = function(val) { - cur_text.font_size = val; - changeSelectedAttribute("font-size", val); - if(!selectedElements[0].textContent) { - textActions.setCursor(); - } -}; - -// Function: getText -// Returns the current text (textContent) of the selected element -this.getText = function() { - var selected = selectedElements[0]; - if (selected == null) { return ""; } - return selected.textContent; -}; - -// Function: setTextContent -// Updates the text element with the given string -// -// Parameters: -// val - String with the new text -this.setTextContent = function(val) { - changeSelectedAttribute("#text", val); - textActions.init(val); - textActions.setCursor(); -}; - -// Function: setImageURL -// Sets the new image URL for the selected image element. Updates its size if -// a new URL is given -// -// Parameters: -// val - String with the image URL/path -this.setImageURL = function(val) { - var elem = selectedElements[0]; - if(!elem) return; - - var attrs = $(elem).attr(['width', 'height']); - var setsize = (!attrs.width || !attrs.height); - - var cur_href = getHref(elem); - - // Do nothing if no URL change or size change - if(cur_href !== val) { - setsize = true; - } else if(!setsize) return; - - var batchCmd = new BatchCommand("Change Image URL"); - - setHref(elem, val); - batchCmd.addSubCommand(new ChangeElementCommand(elem, { - "#href": cur_href - })); - - if(setsize) { - $(new Image()).load(function() { - var changes = $(elem).attr(['width', 'height']); - - $(elem).attr({ - width: this.width, - height: this.height - }); - - selectorManager.requestSelector(elem).resize(); - - batchCmd.addSubCommand(new ChangeElementCommand(elem, changes)); - addCommandToHistory(batchCmd); - call("changed", [elem]); - }).attr('src',val); - } else { - addCommandToHistory(batchCmd); - } -}; - -// Function: setLinkURL -// Sets the new link URL for the selected anchor element. -// -// Parameters: -// val - String with the link URL/path -this.setLinkURL = function(val) { - var elem = selectedElements[0]; - if(!elem) return; - if(elem.tagName !== 'a') { - // See if parent is an anchor - var parents_a = $(elem).parents('a'); - if(parents_a.length) { - elem = parents_a[0]; - } else { - return; - } - } - - var cur_href = getHref(elem); - - if(cur_href === val) return; - - var batchCmd = new BatchCommand("Change Link URL"); - - setHref(elem, val); - batchCmd.addSubCommand(new ChangeElementCommand(elem, { - "#href": cur_href - })); - - addCommandToHistory(batchCmd); -}; - - -// Function: setRectRadius -// Sets the rx & ry values to the selected rect element to change its corner radius -// -// Parameters: -// val - The new radius -this.setRectRadius = function(val) { - var selected = selectedElements[0]; - if (selected != null && selected.tagName == "rect") { - var r = selected.getAttribute("rx"); - if (r != val) { - selected.setAttribute("rx", val); - selected.setAttribute("ry", val); - addCommandToHistory(new ChangeElementCommand(selected, {"rx":r, "ry":r}, "Radius")); - call("changed", [selected]); - } - } -}; - -// Function: makeHyperlink -// Wraps the selected element(s) in an anchor element or converts group to one -this.makeHyperlink = function(url) { - canvas.groupSelectedElements('a', url); - - // TODO: If element is a single "g", convert to "a" - // if(selectedElements.length > 1 && selectedElements[1]) { - -} - -// Function: removeHyperlink -this.removeHyperlink = function() { - canvas.ungroupSelectedElement(); -} - -// Group: Element manipulation - -// Function: setSegType -// Sets the new segment type to the selected segment(s). -// -// Parameters: -// new_type - Integer with the new segment type -// See http://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg for list -this.setSegType = function(new_type) { - pathActions.setSegType(new_type); -} - -// TODO(codedread): Remove the getBBox argument and split this function into two. -// Function: convertToPath -// Convert selected element to a path, or get the BBox of an element-as-path -// -// Parameters: -// elem - The DOM element to be converted -// getBBox - Boolean on whether or not to only return the path's BBox -// -// Returns: -// If the getBBox flag is true, the resulting path's bounding box object. -// Otherwise the resulting path element is returned. -this.convertToPath = function(elem, getBBox) { - if(elem == null) { - var elems = selectedElements; - $.each(selectedElements, function(i, elem) { - if(elem) canvas.convertToPath(elem); - }); - return; - } - - if(!getBBox) { - var batchCmd = new BatchCommand("Convert element to Path"); - } - - var attrs = getBBox?{}:{ - "fill": cur_shape.fill, - "fill-opacity": cur_shape.fill_opacity, - "stroke": cur_shape.stroke, - "stroke-width": cur_shape.stroke_width, - "stroke-dasharray": cur_shape.stroke_dasharray, - "stroke-linejoin": cur_shape.stroke_linejoin, - "stroke-linecap": cur_shape.stroke_linecap, - "stroke-opacity": cur_shape.stroke_opacity, - "opacity": cur_shape.opacity, - "visibility":"hidden" - }; - - // any attribute on the element not covered by the above - // TODO: make this list global so that we can properly maintain it - // TODO: what about @transform, @clip-rule, @fill-rule, etc? - $.each(['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path'], function() { - if (elem.getAttribute(this)) { - attrs[this] = elem.getAttribute(this); - } - }); - - var path = addSvgElementFromJson({ - "element": "path", - "attr": attrs - }); - - var eltrans = elem.getAttribute("transform"); - if(eltrans) { - path.setAttribute("transform",eltrans); - } - - var id = elem.id; - var parent = elem.parentNode; - if(elem.nextSibling) { - parent.insertBefore(path, elem); - } else { - parent.appendChild(path); - } - - var d = ''; - - var joinSegs = function(segs) { - $.each(segs, function(j, seg) { - var l = seg[0], pts = seg[1]; - d += l; - for(var i=0; i < pts.length; i+=2) { - d += (pts[i] +','+pts[i+1]) + ' '; - } - }); - } - - // Possibly the cubed root of 6, but 1.81 works best - var num = 1.81; - - switch (elem.tagName) { - case 'ellipse': - case 'circle': - var a = $(elem).attr(['rx', 'ry', 'cx', 'cy']); - var cx = a.cx, cy = a.cy, rx = a.rx, ry = a.ry; - if(elem.tagName == 'circle') { - rx = ry = $(elem).attr('r'); - } - - joinSegs([ - ['M',[(cx-rx),(cy)]], - ['C',[(cx-rx),(cy-ry/num), (cx-rx/num),(cy-ry), (cx),(cy-ry)]], - ['C',[(cx+rx/num),(cy-ry), (cx+rx),(cy-ry/num), (cx+rx),(cy)]], - ['C',[(cx+rx),(cy+ry/num), (cx+rx/num),(cy+ry), (cx),(cy+ry)]], - ['C',[(cx-rx/num),(cy+ry), (cx-rx),(cy+ry/num), (cx-rx),(cy)]], - ['Z',[]] - ]); - break; - case 'path': - d = elem.getAttribute('d'); - break; - case 'line': - var a = $(elem).attr(["x1", "y1", "x2", "y2"]); - d = "M"+a.x1+","+a.y1+"L"+a.x2+","+a.y2; - break; - case 'polyline': - case 'polygon': - d = "M" + elem.getAttribute('points'); - break; - case 'rect': - var r = $(elem).attr(['rx', 'ry']); - var rx = r.rx, ry = r.ry; - var b = elem.getBBox(); - var x = b.x, y = b.y, w = b.width, h = b.height; - var num = 4-num; // Why? Because! - - if(!rx && !ry) { - // Regular rect - joinSegs([ - ['M',[x, y]], - ['L',[x+w, y]], - ['L',[x+w, y+h]], - ['L',[x, y+h]], - ['L',[x, y]], - ['Z',[]] - ]); - } else { - joinSegs([ - ['M',[x, y+ry]], - ['C',[x,y+ry/num, x+rx/num,y, x+rx,y]], - ['L',[x+w-rx, y]], - ['C',[x+w-rx/num,y, x+w,y+ry/num, x+w,y+ry]], - ['L',[x+w, y+h-ry]], - ['C',[x+w, y+h-ry/num, x+w-rx/num,y+h, x+w-rx,y+h]], - ['L',[x+rx, y+h]], - ['C',[x+rx/num, y+h, x,y+h-ry/num, x,y+h-ry]], - ['L',[x, y+ry]], - ['Z',[]] - ]); - } - break; - default: - path.parentNode.removeChild(path); - break; - } - - if(d) { - path.setAttribute('d',d); - } - - if(!getBBox) { - // Replace the current element with the converted one - - // Reorient if it has a matrix - if(eltrans) { - var tlist = getTransformList(path); - if(hasMatrixTransform(tlist)) { - pathActions.resetOrientation(path); - } - } - - var nextSibling = elem.nextSibling; - batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent)); - batchCmd.addSubCommand(new InsertElementCommand(path)); - - clearSelection(); - elem.parentNode.removeChild(elem) - path.setAttribute('id', id); - path.removeAttribute("visibility"); - addToSelection([path], true); - - addCommandToHistory(batchCmd); - - } else { - // Get the correct BBox of the new path, then discard it - pathActions.resetOrientation(path); - var bb = false; - try { - bb = path.getBBox(); - } catch(e) { - // Firefox fails - } - path.parentNode.removeChild(path); - return bb; - } -}; - - -// Function: changeSelectedAttributeNoUndo -// This function makes the changes to the elements. It does not add the change -// to the history stack. -// -// Parameters: -// attr - String with the attribute name -// newValue - String or number with the new attribute value -// elems - The DOM elements to apply the change to -var changeSelectedAttributeNoUndo = function(attr, newValue, elems) { - var handle = svgroot.suspendRedraw(1000); - if(current_mode == 'pathedit') { - // Editing node - pathActions.moveNode(attr, newValue); - } - var elems = elems || selectedElements; - var i = elems.length; - var no_xy_elems = ['g', 'polyline', 'path']; - var good_g_attrs = ['transform', 'opacity', 'filter']; - - while (i--) { - var elem = elems[i]; - if (elem == null) continue; - - // Go into "select" mode for text changes - if(current_mode === "textedit" && attr !== "#text" && elem.textContent.length) { - textActions.toSelectMode(elem); - } - - // Set x,y vals on elements that don't have them - if((attr === 'x' || attr === 'y') && no_xy_elems.indexOf(elem.tagName) >= 0) { - var bbox = getStrokedBBox([elem]); - var diff_x = attr === 'x' ? newValue - bbox.x : 0; - var diff_y = attr === 'y' ? newValue - bbox.y : 0; - canvas.moveSelectedElements(diff_x*current_zoom, diff_y*current_zoom, true); - continue; - } - - // only allow the transform/opacity/filter attribute to change on <g> elements, slightly hacky - // TODO: FIXME: This doesn't seem right. Where's the body of this if statement? - if (elem.tagName === "g" && good_g_attrs.indexOf(attr) >= 0); - var oldval = attr === "#text" ? elem.textContent : elem.getAttribute(attr); - if (oldval == null) oldval = ""; - if (oldval !== String(newValue)) { - if (attr == "#text") { - var old_w = svgedit.utilities.getBBox(elem).width; - elem.textContent = newValue; - - // FF bug occurs on on rotated elements - if(/rotate/.test(elem.getAttribute('transform'))) { - elem = ffClone(elem); - } - - // Hoped to solve the issue of moving text with text-anchor="start", - // but this doesn't actually fix it. Hopefully on the right track, though. -Fyrd - -// var box=getBBox(elem), left=box.x, top=box.y, width=box.width, -// height=box.height, dx = width - old_w, dy=0; -// var angle = getRotationAngle(elem, true); -// if (angle) { -// var r = Math.sqrt( dx*dx + dy*dy ); -// var theta = Math.atan2(dy,dx) - angle; -// dx = r * Math.cos(theta); -// dy = r * Math.sin(theta); -// -// elem.setAttribute('x', elem.getAttribute('x')-dx); -// elem.setAttribute('y', elem.getAttribute('y')-dy); -// } - - } else if (attr == "#href") { - setHref(elem, newValue); - } - else elem.setAttribute(attr, newValue); -// if (i==0) -// selectedBBoxes[0] = svgedit.utilities.getBBox(elem); - // Use the Firefox ffClone hack for text elements with gradients or - // where other text attributes are changed. - if(svgedit.browser.isGecko() && elem.nodeName === 'text' && /rotate/.test(elem.getAttribute('transform'))) { - if((newValue+'').indexOf('url') === 0 || ['font-size','font-family','x','y'].indexOf(attr) >= 0 && elem.textContent) { - elem = ffClone(elem); - } - } - // Timeout needed for Opera & Firefox - // codedread: it is now possible for this function to be called with elements - // that are not in the selectedElements array, we need to only request a - // selector if the element is in that array - if (selectedElements.indexOf(elem) >= 0) { - setTimeout(function() { - // Due to element replacement, this element may no longer - // be part of the DOM - if(!elem.parentNode) return; - selectorManager.requestSelector(elem).resize(); - },0); - } - // if this element was rotated, and we changed the position of this element - // we need to update the rotational transform attribute - var angle = getRotationAngle(elem); - if (angle != 0 && attr != "transform") { - var tlist = getTransformList(elem); - var n = tlist.numberOfItems; - while (n--) { - var xform = tlist.getItem(n); - if (xform.type == 4) { - // remove old rotate - tlist.removeItem(n); - - var box = svgedit.utilities.getBBox(elem); - var center = transformPoint(box.x+box.width/2, box.y+box.height/2, transformListToTransform(tlist).matrix); - var cx = center.x, - cy = center.y; - var newrot = svgroot.createSVGTransform(); - newrot.setRotate(angle, cx, cy); - tlist.insertItemBefore(newrot, n); - break; - } - } - } - } // if oldValue != newValue - } // for each elem - svgroot.unsuspendRedraw(handle); -}; - -// Function: changeSelectedAttribute -// Change the given/selected element and add the original value to the history stack -// If you want to change all selectedElements, ignore the elems argument. -// If you want to change only a subset of selectedElements, then send the -// subset to this function in the elems argument. -// -// Parameters: -// attr - String with the attribute name -// newValue - String or number with the new attribute value -// elems - The DOM elements to apply the change to -var changeSelectedAttribute = this.changeSelectedAttribute = function(attr, val, elems) { - var elems = elems || selectedElements; - canvas.undoMgr.beginUndoableChange(attr, elems); - var i = elems.length; - - changeSelectedAttributeNoUndo(attr, val, elems); - - var batchCmd = canvas.undoMgr.finishUndoableChange(); - if (!batchCmd.isEmpty()) { - addCommandToHistory(batchCmd); - } -}; - -// Function: deleteSelectedElements -// Removes all selected elements from the DOM and adds the change to the -// history stack -this.deleteSelectedElements = function() { - var batchCmd = new BatchCommand("Delete Elements"); - var len = selectedElements.length; - var selectedCopy = []; //selectedElements is being deleted - for (var i = 0; i < len; ++i) { - var selected = selectedElements[i]; - if (selected == null) break; - - var parent = selected.parentNode; - var t = selected; - - // this will unselect the element and remove the selectedOutline - selectorManager.releaseSelector(t); - - // Remove the path if present. - svgedit.path.removePath_(t.id); - - // Get the parent if it's a single-child anchor - if(parent.tagName === 'a' && parent.childNodes.length === 1) { - t = parent; - parent = parent.parentNode; - } - - var nextSibling = t.nextSibling; - var elem = parent.removeChild(t); - selectedCopy.push(selected); //for the copy - selectedElements[i] = null; - batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent)); - } - if (!batchCmd.isEmpty()) addCommandToHistory(batchCmd); - call("changed", selectedCopy); - clearSelection(); -}; - -// Function: cutSelectedElements -// Removes all selected elements from the DOM and adds the change to the -// history stack. Remembers removed elements on the clipboard - -// TODO: Combine similar code with deleteSelectedElements -this.cutSelectedElements = function() { - var batchCmd = new BatchCommand("Cut Elements"); - var len = selectedElements.length; - var selectedCopy = []; //selectedElements is being deleted - for (var i = 0; i < len; ++i) { - var selected = selectedElements[i]; - if (selected == null) break; - - var parent = selected.parentNode; - var t = selected; - - // this will unselect the element and remove the selectedOutline - selectorManager.releaseSelector(t); - - // Remove the path if present. - svgedit.path.removePath_(t.id); - - var nextSibling = t.nextSibling; - var elem = parent.removeChild(t); - selectedCopy.push(selected); //for the copy - selectedElements[i] = null; - batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent)); - } - if (!batchCmd.isEmpty()) addCommandToHistory(batchCmd); - call("changed", selectedCopy); - clearSelection(); - - canvas.clipBoard = selectedCopy; -}; - -// Function: copySelectedElements -// Remembers the current selected elements on the clipboard -this.copySelectedElements = function() { - canvas.clipBoard = $.merge([], selectedElements); -}; - -this.pasteElements = function(type, x, y) { - var cb = canvas.clipBoard; - var len = cb.length; - if(!len) return; - - var pasted = []; - var batchCmd = new BatchCommand('Paste elements'); - - // Move elements to lastClickPoint - - while (len--) { - var elem = cb[len]; - if(!elem) continue; - var copy = copyElem(elem); - - // See if elem with elem ID is in the DOM already - if(!getElem(elem.id)) copy.id = elem.id; - - pasted.push(copy); - (current_group || getCurrentDrawing().getCurrentLayer()).appendChild(copy); - batchCmd.addSubCommand(new InsertElementCommand(copy)); - } - - selectOnly(pasted); - - if(type !== 'in_place') { - - var ctr_x, ctr_y; - - if(!type) { - ctr_x = lastClickPoint.x; - ctr_y = lastClickPoint.y; - } else if(type === 'point') { - ctr_x = x; - ctr_y = y; - } - - var bbox = getStrokedBBox(pasted); - var cx = ctr_x - (bbox.x + bbox.width/2), - cy = ctr_y - (bbox.y + bbox.height/2), - dx = [], - dy = []; - - $.each(pasted, function(i, item) { - dx.push(cx); - dy.push(cy); - }); - - var cmd = canvas.moveSelectedElements(dx, dy, false); - batchCmd.addSubCommand(cmd); - } - - - - addCommandToHistory(batchCmd); - call("changed", pasted); -} - -// Function: groupSelectedElements -// Wraps all the selected elements in a group (g) element - -// Parameters: -// type - type of element to group into, defaults to <g> -this.groupSelectedElements = function(type) { - if(!type) type = 'g'; - var cmd_str = ''; - - switch ( type ) { - case "a": - cmd_str = "Make hyperlink"; - var url = ''; - if(arguments.length > 1) { - url = arguments[1]; - } - break; - default: - type = 'g'; - cmd_str = "Group Elements"; - break; - } - - var batchCmd = new BatchCommand(cmd_str); - - // create and insert the group element - var g = addSvgElementFromJson({ - "element": type, - "attr": { - "id": getNextId() - } - }); - if(type === 'a') { - setHref(g, url); - } - batchCmd.addSubCommand(new InsertElementCommand(g)); - - // now move all children into the group - var i = selectedElements.length; - while (i--) { - var elem = selectedElements[i]; - if (elem == null) continue; - - if (elem.parentNode.tagName === 'a' && elem.parentNode.childNodes.length === 1) { - elem = elem.parentNode; - } - - var oldNextSibling = elem.nextSibling; - var oldParent = elem.parentNode; - g.appendChild(elem); - batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent)); - } - if (!batchCmd.isEmpty()) addCommandToHistory(batchCmd); - - // update selection - selectOnly([g], true); -}; - - -// Function: pushGroupProperties -// Pushes all appropriate parent group properties down to its children, then -// removes them from the group -var pushGroupProperties = this.pushGroupProperties = function(g, undoable) { - - var children = g.childNodes; - var len = children.length; - var xform = g.getAttribute("transform"); - - var glist = getTransformList(g); - var m = transformListToTransform(glist).matrix; - - var batchCmd = new BatchCommand("Push group properties"); - - // TODO: get all fill/stroke properties from the group that we are about to destroy - // "fill", "fill-opacity", "fill-rule", "stroke", "stroke-dasharray", "stroke-dashoffset", - // "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", - // "stroke-width" - // and then for each child, if they do not have the attribute (or the value is 'inherit') - // then set the child's attribute - - var i = 0; - var gangle = getRotationAngle(g); - - var gattrs = $(g).attr(['filter', 'opacity']); - var gfilter, gblur; - - for(var i = 0; i < len; i++) { - var elem = children[i]; - - if(elem.nodeType !== 1) continue; - - if(gattrs.opacity !== null && gattrs.opacity !== 1) { - var c_opac = elem.getAttribute('opacity') || 1; - var new_opac = Math.round((elem.getAttribute('opacity') || 1) * gattrs.opacity * 100)/100; - changeSelectedAttribute('opacity', new_opac, [elem]); - } - - if(gattrs.filter) { - var cblur = this.getBlur(elem); - var orig_cblur = cblur; - if(!gblur) gblur = this.getBlur(g); - if(cblur) { - // Is this formula correct? - cblur = (gblur-0) + (cblur-0); - } else if(cblur === 0) { - cblur = gblur; - } - - // If child has no current filter, get group's filter or clone it. - if(!orig_cblur) { - // Set group's filter to use first child's ID - if(!gfilter) { - gfilter = getRefElem(gattrs.filter); - } else { - // Clone the group's filter - gfilter = copyElem(gfilter); - findDefs().appendChild(gfilter); - } - } else { - gfilter = getRefElem(elem.getAttribute('filter')); - } - - // Change this in future for different filters - var suffix = (gfilter.firstChild.tagName === 'feGaussianBlur')?'blur':'filter'; - gfilter.id = elem.id + '_' + suffix; - changeSelectedAttribute('filter', 'url(#' + gfilter.id + ')', [elem]); - - // Update blur value - if(cblur) { - changeSelectedAttribute('stdDeviation', cblur, [gfilter.firstChild]); - canvas.setBlurOffsets(gfilter, cblur); - } - } - - var chtlist = getTransformList(elem); - - // Don't process gradient transforms - if(~elem.tagName.indexOf('Gradient')) chtlist = null; - - // Hopefully not a problem to add this. Necessary for elements like <desc/> - if(!chtlist) continue; - - // Apparently <defs> can get get a transformlist, but we don't want it to have one! - if(elem.tagName === 'defs') continue; - - if (glist.numberOfItems) { - // TODO: if the group's transform is just a rotate, we can always transfer the - // rotate() down to the children (collapsing consecutive rotates and factoring - // out any translates) - if (gangle && glist.numberOfItems == 1) { - // [Rg] [Rc] [Mc] - // we want [Tr] [Rc2] [Mc] where: - // - [Rc2] is at the child's current center but has the - // sum of the group and child's rotation angles - // - [Tr] is the equivalent translation that this child - // undergoes if the group wasn't there - - // [Tr] = [Rg] [Rc] [Rc2_inv] - - // get group's rotation matrix (Rg) - var rgm = glist.getItem(0).matrix; - - // get child's rotation matrix (Rc) - var rcm = svgroot.createSVGMatrix(); - var cangle = getRotationAngle(elem); - if (cangle) { - rcm = chtlist.getItem(0).matrix; - } - - // get child's old center of rotation - var cbox = svgedit.utilities.getBBox(elem); - var ceqm = transformListToTransform(chtlist).matrix; - var coldc = transformPoint(cbox.x+cbox.width/2, cbox.y+cbox.height/2,ceqm); - - // sum group and child's angles - var sangle = gangle + cangle; - - // get child's rotation at the old center (Rc2_inv) - var r2 = svgroot.createSVGTransform(); - r2.setRotate(sangle, coldc.x, coldc.y); - - // calculate equivalent translate - var trm = matrixMultiply(rgm, rcm, r2.matrix.inverse()); - - // set up tlist - if (cangle) { - chtlist.removeItem(0); - } - - if (sangle) { - if(chtlist.numberOfItems) { - chtlist.insertItemBefore(r2, 0); - } else { - chtlist.appendItem(r2); - } - } - - if (trm.e || trm.f) { - var tr = svgroot.createSVGTransform(); - tr.setTranslate(trm.e, trm.f); - if(chtlist.numberOfItems) { - chtlist.insertItemBefore(tr, 0); - } else { - chtlist.appendItem(tr); - } - } - } - else { // more complicated than just a rotate - - // transfer the group's transform down to each child and then - // call recalculateDimensions() - var oldxform = elem.getAttribute("transform"); - var changes = {}; - changes["transform"] = oldxform ? oldxform : ""; - - var newxform = svgroot.createSVGTransform(); - - // [ gm ] [ chm ] = [ chm ] [ gm' ] - // [ gm' ] = [ chm_inv ] [ gm ] [ chm ] - var chm = transformListToTransform(chtlist).matrix, - chm_inv = chm.inverse(); - var gm = matrixMultiply( chm_inv, m, chm ); - newxform.setMatrix(gm); - chtlist.appendItem(newxform); - } - var cmd = recalculateDimensions(elem); - if(cmd) batchCmd.addSubCommand(cmd); - } - } - - - // remove transform and make it undo-able - if (xform) { - var changes = {}; - changes["transform"] = xform; - g.setAttribute("transform", ""); - g.removeAttribute("transform"); - batchCmd.addSubCommand(new ChangeElementCommand(g, changes)); - } - - if (undoable && !batchCmd.isEmpty()) { - return batchCmd; - } -} - - -// Function: ungroupSelectedElement -// Unwraps all the elements in a selected group (g) element. This requires -// significant recalculations to apply group's transforms, etc to its children -this.ungroupSelectedElement = function() { - var g = selectedElements[0]; - if($(g).data('gsvg') || $(g).data('symbol')) { - // Is svg, so actually convert to group - - convertToGroup(g); - return; - } else if(g.tagName === 'use') { - // Somehow doesn't have data set, so retrieve - var symbol = getElem(getHref(g).substr(1)); - $(g).data('symbol', symbol).data('ref', symbol); - convertToGroup(g); - return; - } - var parents_a = $(g).parents('a'); - if(parents_a.length) { - g = parents_a[0]; - } - - // Look for parent "a" - if (g.tagName === "g" || g.tagName === "a") { - - var batchCmd = new BatchCommand("Ungroup Elements"); - var cmd = pushGroupProperties(g, true); - if(cmd) batchCmd.addSubCommand(cmd); - - var parent = g.parentNode; - var anchor = g.nextSibling; - var children = new Array(g.childNodes.length); - - var i = 0; - - while (g.firstChild) { - var elem = g.firstChild; - var oldNextSibling = elem.nextSibling; - var oldParent = elem.parentNode; - - // Remove child title elements - if(elem.tagName === 'title') { - var nextSibling = elem.nextSibling; - batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, oldParent)); - oldParent.removeChild(elem); - continue; - } - - children[i++] = elem = parent.insertBefore(elem, anchor); - batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent)); - } - - // remove the group from the selection - clearSelection(); - - // delete the group element (but make undo-able) - var gNextSibling = g.nextSibling; - g = parent.removeChild(g); - batchCmd.addSubCommand(new RemoveElementCommand(g, gNextSibling, parent)); - - if (!batchCmd.isEmpty()) addCommandToHistory(batchCmd); - - // update selection - addToSelection(children); - } -}; - -// Function: moveToTopSelectedElement -// Repositions the selected element to the bottom in the DOM to appear on top of -// other elements -this.moveToTopSelectedElement = function() { - var selected = selectedElements[0]; - if (selected != null) { - var t = selected; - var oldParent = t.parentNode; - var oldNextSibling = t.nextSibling; - t = t.parentNode.appendChild(t); - // If the element actually moved position, add the command and fire the changed - // event handler. - if (oldNextSibling != t.nextSibling) { - addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, "top")); - call("changed", [t]); - } - } -}; - -// Function: moveToBottomSelectedElement -// Repositions the selected element to the top in the DOM to appear under -// other elements -this.moveToBottomSelectedElement = function() { - var selected = selectedElements[0]; - if (selected != null) { - var t = selected; - var oldParent = t.parentNode; - var oldNextSibling = t.nextSibling; - var firstChild = t.parentNode.firstChild; - if (firstChild.tagName == 'title') { - firstChild = firstChild.nextSibling; - } - // This can probably be removed, as the defs should not ever apppear - // inside a layer group - if (firstChild.tagName == 'defs') { - firstChild = firstChild.nextSibling; - } - t = t.parentNode.insertBefore(t, firstChild); - // If the element actually moved position, add the command and fire the changed - // event handler. - if (oldNextSibling != t.nextSibling) { - addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, "bottom")); - call("changed", [t]); - } - } -}; - -// Function: moveUpDownSelected -// Moves the select element up or down the stack, based on the visibly -// intersecting elements -// -// Parameters: -// dir - String that's either 'Up' or 'Down' -this.moveUpDownSelected = function(dir) { - var selected = selectedElements[0]; - if (!selected) return; - - curBBoxes = []; - var closest, found_cur; - // jQuery sorts this list - var list = $(getIntersectionList(getStrokedBBox([selected]))).toArray(); - if(dir == 'Down') list.reverse(); - - $.each(list, function() { - if(!found_cur) { - if(this == selected) { - found_cur = true; - } - return; - } - closest = this; - return false; - }); - if(!closest) return; - - var t = selected; - var oldParent = t.parentNode; - var oldNextSibling = t.nextSibling; - $(closest)[dir == 'Down'?'before':'after'](t); - // If the element actually moved position, add the command and fire the changed - // event handler. - if (oldNextSibling != t.nextSibling) { - addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, "Move " + dir)); - call("changed", [t]); - } -}; - -// Function: moveSelectedElements -// Moves selected elements on the X/Y axis -// -// Parameters: -// dx - Float with the distance to move on the x-axis -// dy - Float with the distance to move on the y-axis -// undoable - Boolean indicating whether or not the action should be undoable -// -// Returns: -// Batch command for the move -this.moveSelectedElements = function(dx, dy, undoable) { - // if undoable is not sent, default to true - // if single values, scale them to the zoom - if (dx.constructor != Array) { - dx /= current_zoom; - dy /= current_zoom; - } - var undoable = undoable || true; - var batchCmd = new BatchCommand("position"); - var i = selectedElements.length; - while (i--) { - var selected = selectedElements[i]; - if (selected != null) { -// if (i==0) -// selectedBBoxes[0] = svgedit.utilities.getBBox(selected); - -// var b = {}; -// for(var j in selectedBBoxes[i]) b[j] = selectedBBoxes[i][j]; -// selectedBBoxes[i] = b; - - var xform = svgroot.createSVGTransform(); - var tlist = getTransformList(selected); - - // dx and dy could be arrays - if (dx.constructor == Array) { -// if (i==0) { -// selectedBBoxes[0].x += dx[0]; -// selectedBBoxes[0].y += dy[0]; -// } - xform.setTranslate(dx[i],dy[i]); - } else { -// if (i==0) { -// selectedBBoxes[0].x += dx; -// selectedBBoxes[0].y += dy; -// } - xform.setTranslate(dx,dy); - } - - if(tlist.numberOfItems) { - tlist.insertItemBefore(xform, 0); - } else { - tlist.appendItem(xform); - } - - var cmd = recalculateDimensions(selected); - if (cmd) { - batchCmd.addSubCommand(cmd); - } - - selectorManager.requestSelector(selected).resize(); - } - } - if (!batchCmd.isEmpty()) { - if (undoable) - addCommandToHistory(batchCmd); - call("changed", selectedElements); - return batchCmd; - } -}; - -// Function: cloneSelectedElements -// Create deep DOM copies (clones) of all selected elements and move them slightly -// from their originals -this.cloneSelectedElements = function(x,y) { - var batchCmd = new BatchCommand("Clone Elements"); - // find all the elements selected (stop at first null) - var len = selectedElements.length; - for (var i = 0; i < len; ++i) { - var elem = selectedElements[i]; - if (elem == null) break; - } - // use slice to quickly get the subset of elements we need - var copiedElements = selectedElements.slice(0,i); - this.clearSelection(true); - // note that we loop in the reverse way because of the way elements are added - // to the selectedElements array (top-first) - var i = copiedElements.length; - while (i--) { - // clone each element and replace it within copiedElements - var elem = copiedElements[i] = copyElem(copiedElements[i]); - (current_group || getCurrentDrawing().getCurrentLayer()).appendChild(elem); - batchCmd.addSubCommand(new InsertElementCommand(elem)); - } - - if (!batchCmd.isEmpty()) { - addToSelection(copiedElements.reverse()); // Need to reverse for correct selection-adding - this.moveSelectedElements(x,y,false); - addCommandToHistory(batchCmd); - } -}; - -// Function: alignSelectedElements -// Aligns selected elements -// -// Parameters: -// type - String with single character indicating the alignment type -// relative_to - String that must be one of the following: -// "selected", "largest", "smallest", "page" -this.alignSelectedElements = function(type, relative_to) { - var bboxes = [], angles = []; - var minx = Number.MAX_VALUE, maxx = Number.MIN_VALUE, miny = Number.MAX_VALUE, maxy = Number.MIN_VALUE; - var curwidth = Number.MIN_VALUE, curheight = Number.MIN_VALUE; - var len = selectedElements.length; - if (!len) return; - for (var i = 0; i < len; ++i) { - if (selectedElements[i] == null) break; - var elem = selectedElements[i]; - bboxes[i] = getStrokedBBox([elem]); - - // now bbox is axis-aligned and handles rotation - switch (relative_to) { - case 'smallest': - if ( (type == 'l' || type == 'c' || type == 'r') && (curwidth == Number.MIN_VALUE || curwidth > bboxes[i].width) || - (type == 't' || type == 'm' || type == 'b') && (curheight == Number.MIN_VALUE || curheight > bboxes[i].height) ) { - minx = bboxes[i].x; - miny = bboxes[i].y; - maxx = bboxes[i].x + bboxes[i].width; - maxy = bboxes[i].y + bboxes[i].height; - curwidth = bboxes[i].width; - curheight = bboxes[i].height; - } - break; - case 'largest': - if ( (type == 'l' || type == 'c' || type == 'r') && (curwidth == Number.MIN_VALUE || curwidth < bboxes[i].width) || - (type == 't' || type == 'm' || type == 'b') && (curheight == Number.MIN_VALUE || curheight < bboxes[i].height) ) { - minx = bboxes[i].x; - miny = bboxes[i].y; - maxx = bboxes[i].x + bboxes[i].width; - maxy = bboxes[i].y + bboxes[i].height; - curwidth = bboxes[i].width; - curheight = bboxes[i].height; - } - break; - default: // 'selected' - if (bboxes[i].x < minx) minx = bboxes[i].x; - if (bboxes[i].y < miny) miny = bboxes[i].y; - if (bboxes[i].x + bboxes[i].width > maxx) maxx = bboxes[i].x + bboxes[i].width; - if (bboxes[i].y + bboxes[i].height > maxy) maxy = bboxes[i].y + bboxes[i].height; - break; - } - } // loop for each element to find the bbox and adjust min/max - - if (relative_to == 'page') { - minx = 0; - miny = 0; - maxx = canvas.contentW; - maxy = canvas.contentH; - } - - var dx = new Array(len); - var dy = new Array(len); - for (var i = 0; i < len; ++i) { - if (selectedElements[i] == null) break; - var elem = selectedElements[i]; - var bbox = bboxes[i]; - dx[i] = 0; - dy[i] = 0; - switch (type) { - case 'l': // left (horizontal) - dx[i] = minx - bbox.x; - break; - case 'c': // center (horizontal) - dx[i] = (minx+maxx)/2 - (bbox.x + bbox.width/2); - break; - case 'r': // right (horizontal) - dx[i] = maxx - (bbox.x + bbox.width); - break; - case 't': // top (vertical) - dy[i] = miny - bbox.y; - break; - case 'm': // middle (vertical) - dy[i] = (miny+maxy)/2 - (bbox.y + bbox.height/2); - break; - case 'b': // bottom (vertical) - dy[i] = maxy - (bbox.y + bbox.height); - break; - } - } - this.moveSelectedElements(dx,dy); -}; - -// Group: Additional editor tools - -this.contentW = getResolution().w; -this.contentH = getResolution().h; - -// Function: updateCanvas -// Updates the editor canvas width/height/position after a zoom has occurred -// -// Parameters: -// w - Float with the new width -// h - Float with the new height -// -// Returns: -// Object with the following values: -// * x - The canvas' new x coordinate -// * y - The canvas' new y coordinate -// * old_x - The canvas' old x coordinate -// * old_y - The canvas' old y coordinate -// * d_x - The x position difference -// * d_y - The y position difference -this.updateCanvas = function(w, h) { - svgroot.setAttribute("width", w); - svgroot.setAttribute("height", h); - var bg = $('#canvasBackground')[0]; - var old_x = svgcontent.getAttribute('x'); - var old_y = svgcontent.getAttribute('y'); - var x = (w/2 - this.contentW*current_zoom/2); - var y = (h/2 - this.contentH*current_zoom/2); - - assignAttributes(svgcontent, { - width: this.contentW*current_zoom, - height: this.contentH*current_zoom, - 'x': x, - 'y': y, - "viewBox" : "0 0 " + this.contentW + " " + this.contentH - }); - - assignAttributes(bg, { - width: svgcontent.getAttribute('width'), - height: svgcontent.getAttribute('height'), - x: x, - y: y - }); - - var bg_img = getElem('background_image'); - if (bg_img) { - assignAttributes(bg_img, { - 'width': '100%', - 'height': '100%' - }); - } - - selectorManager.selectorParentGroup.setAttribute("transform","translate(" + x + "," + y + ")"); - - return {x:x, y:y, old_x:old_x, old_y:old_y, d_x:x - old_x, d_y:y - old_y}; -} - -// Function: setBackground -// Set the background of the editor (NOT the actual document) -// -// Parameters: -// color - String with fill color to apply -// url - URL or path to image to use -this.setBackground = function(color, url) { - var bg = getElem('canvasBackground'); - var border = $(bg).find('rect')[0]; - var bg_img = getElem('background_image'); - border.setAttribute('fill',color); - if(url) { - if(!bg_img) { - bg_img = svgdoc.createElementNS(svgns, "image"); - assignAttributes(bg_img, { - 'id': 'background_image', - 'width': '100%', - 'height': '100%', - 'preserveAspectRatio': 'xMinYMin', - 'style':'pointer-events:none' - }); - } - setHref(bg_img, url); - bg.appendChild(bg_img); - } else if(bg_img) { - bg_img.parentNode.removeChild(bg_img); - } -} - -// Function: cycleElement -// Select the next/previous element within the current layer -// -// Parameters: -// next - Boolean where true = next and false = previous element -this.cycleElement = function(next) { - var cur_elem = selectedElements[0]; - var elem = false; - var all_elems = getVisibleElements(current_group || getCurrentDrawing().getCurrentLayer()); - if(!all_elems.length) return; - if (cur_elem == null) { - var num = next?all_elems.length-1:0; - elem = all_elems[num]; - } else { - var i = all_elems.length; - while(i--) { - if(all_elems[i] == cur_elem) { - var num = next?i-1:i+1; - if(num >= all_elems.length) { - num = 0; - } else if(num < 0) { - num = all_elems.length-1; - } - elem = all_elems[num]; - break; - } - } - } - selectOnly([elem], true); - call("selected", selectedElements); -} - -this.clear(); - - -// DEPRECATED: getPrivateMethods -// Since all methods are/should be public somehow, this function should be removed - -// Being able to access private methods publicly seems wrong somehow, -// but currently appears to be the best way to allow testing and provide -// access to them to plugins. -this.getPrivateMethods = function() { - var obj = { - addCommandToHistory: addCommandToHistory, - setGradient: setGradient, - addSvgElementFromJson: addSvgElementFromJson, - assignAttributes: assignAttributes, - BatchCommand: BatchCommand, - call: call, - ChangeElementCommand: ChangeElementCommand, - copyElem: copyElem, - ffClone: ffClone, - findDefs: findDefs, - findDuplicateGradient: findDuplicateGradient, - getElem: getElem, - getId: getId, - getIntersectionList: getIntersectionList, - getMouseTarget: getMouseTarget, - getNextId: getNextId, - getPathBBox: getPathBBox, - getUrlFromAttr: getUrlFromAttr, - hasMatrixTransform: hasMatrixTransform, - identifyLayers: identifyLayers, - InsertElementCommand: InsertElementCommand, - isIdentity: svgedit.math.isIdentity, - logMatrix: logMatrix, - matrixMultiply: matrixMultiply, - MoveElementCommand: MoveElementCommand, - preventClickDefault: preventClickDefault, - recalculateAllSelectedDimensions: recalculateAllSelectedDimensions, - recalculateDimensions: recalculateDimensions, - remapElement: remapElement, - RemoveElementCommand: RemoveElementCommand, - removeUnusedDefElems: removeUnusedDefElems, - round: round, - runExtensions: runExtensions, - sanitizeSvg: sanitizeSvg, - SVGEditTransformList: svgedit.transformlist.SVGTransformList, - toString: toString, - transformBox: svgedit.math.transformBox, - transformListToTransform: transformListToTransform, - transformPoint: transformPoint, - walkTree: svgedit.utilities.walkTree - } - return obj; -}; - -} diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgedit.compiled.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgedit.compiled.js deleted file mode 100644 index 710880cd..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgedit.compiled.js +++ /dev/null @@ -1,466 +0,0 @@ -jQuery&&function(){var a=$(window),J=$(document);$.extend($.fn,{contextMenu:function(l,s){if(l.menu==undefined)return false;if(l.inSpeed==undefined)l.inSpeed=150;if(l.outSpeed==undefined)l.outSpeed=75;if(l.inSpeed==0)l.inSpeed=-1;if(l.outSpeed==0)l.outSpeed=-1;$(this).each(function(){var v=$(this),G=$(v).offset(),e=$("#"+l.menu);e.addClass("contextMenu");$(this).bind("mousedown",function(f){$(this).mouseup(function(k){var n=$(this);n.unbind("mouseup");if(f.button===2||l.allowLeft||f.ctrlKey&&svgedit.browser.isMac()){k.stopPropagation(); -$(".contextMenu").hide();if(v.hasClass("disabled"))return false;var F=k.pageX,B=k.pageY;k=a.width()-e.width();var A=a.height()-e.height();if(F>k-15)F=k-15;if(B>A-30)B=A-30;J.unbind("click");e.css({top:B,left:F}).fadeIn(l.inSpeed);e.find("A").mouseover(function(){e.find("LI.hover").removeClass("hover");$(this).parent().addClass("hover")}).mouseout(function(){e.find("LI.hover").removeClass("hover")});J.keypress(function(O){switch(O.keyCode){case 38:if(e.find("LI.hover").length){e.find("LI.hover").removeClass("hover").prevAll("LI:not(.disabled)").eq(0).addClass("hover"); -e.find("LI.hover").length||e.find("LI:last").addClass("hover")}else e.find("LI:last").addClass("hover");break;case 40:if(e.find("LI.hover").length==0)e.find("LI:first").addClass("hover");else{e.find("LI.hover").removeClass("hover").nextAll("LI:not(.disabled)").eq(0).addClass("hover");e.find("LI.hover").length||e.find("LI:first").addClass("hover")}break;case 13:e.find("LI.hover A").trigger("click");break;case 27:J.trigger("click")}});e.find("A").unbind("mouseup");e.find("LI:not(.disabled) A").mouseup(function(){J.unbind("click").unbind("keypress"); -$(".contextMenu").hide();s&&s($(this).attr("href").substr(1),$(n),{x:F-G.left,y:B-G.top,docX:F,docY:B});return false});setTimeout(function(){J.click(function(){J.unbind("click").unbind("keypress");e.fadeOut(l.outSpeed);return false})},0)}})});if($.browser.mozilla)$("#"+l.menu).each(function(){$(this).css({MozUserSelect:"none"})});else $.browser.msie?$("#"+l.menu).each(function(){$(this).bind("selectstart.disableTextSelect",function(){return false})}):$("#"+l.menu).each(function(){$(this).bind("mousedown.disableTextSelect", -function(){return false})});$(v).add($("UL.contextMenu")).bind("contextmenu",function(){return false})});return $(this)},disableContextMenuItems:function(l){if(l==undefined){$(this).find("LI").addClass("disabled");return $(this)}$(this).each(function(){if(l!=undefined)for(var s=l.split(","),v=0;v<s.length;v++)$(this).find('A[href="'+s[v]+'"]').parent().addClass("disabled")});return $(this)},enableContextMenuItems:function(l){if(l==undefined){$(this).find("LI.disabled").removeClass("disabled");return $(this)}$(this).each(function(){if(l!= -undefined)for(var s=l.split(","),v=0;v<s.length;v++)$(this).find('A[href="'+s[v]+'"]').parent().removeClass("disabled")});return $(this)},disableContextMenu:function(){$(this).each(function(){$(this).addClass("disabled")});return $(this)},enableContextMenu:function(){$(this).each(function(){$(this).removeClass("disabled")});return $(this)},destroyContextMenu:function(){$(this).each(function(){$(this).unbind("mousedown").unbind("mouseup")});return $(this)}})}(jQuery);var svgedit=svgedit||{}; -(function(){if(!svgedit.browser)svgedit.browser={};var a=!!document.createElementNS&&!!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect;svgedit.browser.supportsSvg=function(){return a};if(svgedit.browser.supportsSvg()){var J=navigator.userAgent,l=document.createElementNS("http://www.w3.org/2000/svg","svg"),s=!!window.opera,v=J.indexOf("AppleWebKit")>=0,G=J.indexOf("Gecko/")>=0,e=J.indexOf("MSIE")>=0,f=J.indexOf("Chrome/")>=0,k=J.indexOf("Windows")>=0,n=J.indexOf("Macintosh")>= -0,F=!!l.querySelector,B=!!document.evaluate,A=function(){var da=document.createElementNS("http://www.w3.org/2000/svg","path");da.setAttribute("d","M0,0 10,10");var na=da.pathSegList;da=da.createSVGPathSegLinetoAbs(5,5);try{na.replaceItem(da,0);return true}catch(ka){}return false}(),O=function(){var da=document.createElementNS("http://www.w3.org/2000/svg","path");da.setAttribute("d","M0,0 10,10");var na=da.pathSegList;da=da.createSVGPathSegLinetoAbs(5,5);try{na.insertItemBefore(da,0);return true}catch(ka){}return false}(), -Z=function(){var da=document.createElementNS("http://www.w3.org/2000/svg","svg"),na=document.createElementNS("http://www.w3.org/2000/svg","svg");document.documentElement.appendChild(da);na.setAttribute("x",5);da.appendChild(na);var ka=document.createElementNS("http://www.w3.org/2000/svg","text");ka.textContent="a";na.appendChild(ka);na=ka.getStartPositionOfChar(0).x;document.documentElement.removeChild(da);return na===0}(),N=function(){var da=document.createElementNS("http://www.w3.org/2000/svg", -"svg");document.documentElement.appendChild(da);var na=document.createElementNS("http://www.w3.org/2000/svg","path");na.setAttribute("d","M0,0 C0,0 10,10 10,0");da.appendChild(na);na=na.getBBox();document.documentElement.removeChild(da);return na.height>4&&na.height<5}(),L=function(){var da=document.createElementNS("http://www.w3.org/2000/svg","svg");document.documentElement.appendChild(da);var na=document.createElementNS("http://www.w3.org/2000/svg","path");na.setAttribute("d","M0,0 10,0");var ka= -document.createElementNS("http://www.w3.org/2000/svg","path");ka.setAttribute("d","M5,0 15,0");var Ra=document.createElementNS("http://www.w3.org/2000/svg","g");Ra.appendChild(na);Ra.appendChild(ka);da.appendChild(Ra);na=Ra.getBBox();document.documentElement.removeChild(da);return na.width==15}(),qa=function(){var da=document.createElementNS("http://www.w3.org/2000/svg","rect");da.setAttribute("x",0.1);(da=da.cloneNode(false).getAttribute("x").indexOf(",")==-1)||$.alert("NOTE: This version of Opera is known to contain bugs in SVG-edit.\n\t\tPlease upgrade to the <a href='http://opera.com'>latest version</a> in which the problems have been fixed."); -return da}(),aa=function(){var da=document.createElementNS("http://www.w3.org/2000/svg","rect");da.setAttribute("style","vector-effect:non-scaling-stroke");return da.style.vectorEffect==="non-scaling-stroke"}(),ja=function(){var da=document.createElementNS("http://www.w3.org/2000/svg","rect").transform.baseVal,na=l.createSVGTransform();da.appendItem(na);return da.getItem(0)==na}();svgedit.browser.isOpera=function(){return s};svgedit.browser.isWebkit=function(){return v};svgedit.browser.isGecko=function(){return G}; -svgedit.browser.isIE=function(){return e};svgedit.browser.isChrome=function(){return f};svgedit.browser.isWindows=function(){return k};svgedit.browser.isMac=function(){return n};svgedit.browser.isTouch=function(){return false};svgedit.browser.supportsSelectors=function(){return F};svgedit.browser.supportsXpath=function(){return B};svgedit.browser.supportsPathReplaceItem=function(){return A};svgedit.browser.supportsPathInsertItemBefore=function(){return O};svgedit.browser.supportsPathBBox=function(){return N}; -svgedit.browser.supportsHVLineContainerBBox=function(){return L};svgedit.browser.supportsGoodTextCharPos=function(){return Z};svgedit.browser.supportsEditableText=function(){return s};svgedit.browser.supportsGoodDecimals=function(){return qa};svgedit.browser.supportsNonScalingStroke=function(){return aa};svgedit.browser.supportsNativeTransformLists=function(){return ja}}else window.location="browser-not-supported.html"})();svgedit=svgedit||{}; -(function(){if(!svgedit.transformlist)svgedit.transformlist={};var a=document.createElementNS("http://www.w3.org/2000/svg","svg"),J={};svgedit.transformlist.SVGTransformList=function(l){this._elem=l||null;this._xforms=[];this._update=function(){var s="";a.createSVGMatrix();for(var v=0;v<this.numberOfItems;++v){var G=this._list.getItem(v);s=s;G=G;var e=G.matrix,f="";switch(G.type){case 1:f="matrix("+[e.a,e.b,e.c,e.d,e.e,e.f].join(",")+")";break;case 2:f="translate("+e.e+","+e.f+")";break;case 3:f= -e.a==e.d?"scale("+e.a+")":"scale("+e.a+","+e.d+")";break;case 4:var k=0;f=0;if(G.angle!=0){k=1-e.a;f=(k*e.f+e.b*e.e)/(k*k+e.b*e.b);k=(e.e-e.b*f)/k}f="rotate("+G.angle+" "+k+","+f+")"}s=s+(f+" ")}this._elem.setAttribute("transform",s)};this._list=this;this._init=function(){var s=this._elem.getAttribute("transform");if(s)for(var v=/\s*((scale|matrix|rotate|translate)\s*\(.*?\))\s*,?\s*/,G=true;G;){G=s.match(v);s=s.replace(v,"");if(G&&G[1]){var e=G[1].split(/\s*\(/),f=e[0];e=e[1].match(/\s*(.*?)\s*\)/); -e[1]=e[1].replace(/(\d)-/g,"$1 -");var k=e[1].split(/[, ]+/),n="abcdef".split(""),F=a.createSVGMatrix();$.each(k,function(O,Z){k[O]=parseFloat(Z);if(f=="matrix")F[n[O]]=k[O]});e=a.createSVGTransform();var B="set"+f.charAt(0).toUpperCase()+f.slice(1),A=f=="matrix"?[F]:k;if(f=="scale"&&A.length==1)A.push(A[0]);else if(f=="translate"&&A.length==1)A.push(0);else if(f=="rotate"&&A.length==1){A.push(0);A.push(0)}e[B].apply(e,A);this._list.appendItem(e)}}};this._removeFromOtherLists=function(s){if(s){var v= -false,G;for(G in J){for(var e=J[G],f=0,k=e._xforms.length;f<k;++f)if(e._xforms[f]==s){v=true;e.removeItem(f);break}if(v)break}}};this.numberOfItems=0;this.clear=function(){this.numberOfItems=0;this._xforms=[]};this.initialize=function(s){this.numberOfItems=1;this._removeFromOtherLists(s);this._xforms=[s]};this.getItem=function(s){if(s<this.numberOfItems&&s>=0)return this._xforms[s];throw{code:1};};this.insertItemBefore=function(s,v){var G=null;if(v>=0)if(v<this.numberOfItems){this._removeFromOtherLists(s); -G=Array(this.numberOfItems+1);for(var e=0;e<v;++e)G[e]=this._xforms[e];G[e]=s;for(var f=e+1;e<this.numberOfItems;++f,++e)G[f]=this._xforms[e];this.numberOfItems++;this._xforms=G;G=s;this._list._update()}else G=this._list.appendItem(s);return G};this.replaceItem=function(s,v){var G=null;if(v<this.numberOfItems&&v>=0){this._removeFromOtherLists(s);G=this._xforms[v]=s;this._list._update()}return G};this.removeItem=function(s){if(s<this.numberOfItems&&s>=0){for(var v=this._xforms[s],G=Array(this.numberOfItems- -1),e=0;e<s;++e)G[e]=this._xforms[e];for(s=e;s<this.numberOfItems-1;++s,++e)G[s]=this._xforms[e+1];this.numberOfItems--;this._xforms=G;this._list._update();return v}else throw{code:1};};this.appendItem=function(s){this._removeFromOtherLists(s);this._xforms.push(s);this.numberOfItems++;this._list._update();return s}};svgedit.transformlist.resetListMap=function(){J={}};svgedit.transformlist.removeElementFromListMap=function(l){l.id&&J[l.id]&&delete J[l.id]};svgedit.transformlist.getTransformList=function(l){if(svgedit.browser.supportsNativeTransformLists())if(l.transform)return l.transform.baseVal; -else if(l.gradientTransform)return l.gradientTransform.baseVal;else{if(l.patternTransform)return l.patternTransform.baseVal}else{var s=l.id;s||(s="temp");var v=J[s];if(!v||s=="temp"){J[s]=new svgedit.transformlist.SVGTransformList(l);J[s]._init();v=J[s]}return v}return null}})();svgedit=svgedit||{}; -(function(){if(!svgedit.math)svgedit.math={};var a=document.createElementNS("http://www.w3.org/2000/svg","svg");svgedit.math.transformPoint=function(J,l,s){return{x:s.a*J+s.c*l+s.e,y:s.b*J+s.d*l+s.f}};svgedit.math.isIdentity=function(J){return J.a===1&&J.b===0&&J.c===0&&J.d===1&&J.e===0&&J.f===0};svgedit.math.matrixMultiply=function(){for(var J=arguments,l=J.length,s=J[l-1];l-- >1;)s=J[l-1].multiply(s);if(Math.abs(s.a)<1.0E-14)s.a=0;if(Math.abs(s.b)<1.0E-14)s.b=0;if(Math.abs(s.c)<1.0E-14)s.c=0;if(Math.abs(s.d)< -1.0E-14)s.d=0;if(Math.abs(s.e)<1.0E-14)s.e=0;if(Math.abs(s.f)<1.0E-14)s.f=0;return s};svgedit.math.hasMatrixTransform=function(J){if(!J)return false;for(var l=J.numberOfItems;l--;){var s=J.getItem(l);if(s.type==1&&!svgedit.math.isIdentity(s.matrix))return true}return false};svgedit.math.transformBox=function(J,l,s,v,G){var e={x:J,y:l},f={x:J+s,y:l};s={x:J+s,y:l+v};J={x:J,y:l+v};l=svgedit.math.transformPoint;e=l(e.x,e.y,G);var k=v=e.x,n=e.y,F=e.y;f=l(f.x,f.y,G);v=Math.min(v,f.x);k=Math.max(k,f.x); -n=Math.min(n,f.y);F=Math.max(F,f.y);J=l(J.x,J.y,G);v=Math.min(v,J.x);k=Math.max(k,J.x);n=Math.min(n,J.y);F=Math.max(F,J.y);s=l(s.x,s.y,G);v=Math.min(v,s.x);k=Math.max(k,s.x);n=Math.min(n,s.y);F=Math.max(F,s.y);return{tl:e,tr:f,bl:J,br:s,aabox:{x:v,y:n,width:k-v,height:F-n}}};svgedit.math.transformListToTransform=function(J,l,s){if(J==null)return a.createSVGTransformFromMatrix(a.createSVGMatrix());l=l==undefined?0:l;s=s==undefined?J.numberOfItems-1:s;l=parseInt(l);s=parseInt(s);if(l>s){var v=s;s=l; -l=v}v=a.createSVGMatrix();for(l=l;l<=s;++l){var G=l>=0&&l<J.numberOfItems?J.getItem(l).matrix:a.createSVGMatrix();v=svgedit.math.matrixMultiply(v,G)}return a.createSVGTransformFromMatrix(v)};svgedit.math.getMatrix=function(J){J=svgedit.transformlist.getTransformList(J);return svgedit.math.transformListToTransform(J).matrix};svgedit.math.snapToAngle=function(J,l,s,v){var G=Math.PI/4;s=s-J;var e=v-l;v=Math.sqrt(s*s+e*e);G=Math.round(Math.atan2(e,s)/G)*G;return{x:J+v*Math.cos(G),y:l+v*Math.sin(G),a:G}}; -svgedit.math.rectsIntersect=function(J,l){return l.x<J.x+J.width&&l.x+l.width>J.x&&l.y<J.y+J.height&&l.y+l.height>J.y}})();svgedit=svgedit||{}; -(function(){if(!svgedit.units)svgedit.units={};var a=["x","x1","cx","rx","width"],J=["y","y1","cy","ry","height"],l=$.merge(["r","radius"],a);$.merge(l,J);var s,v={px:1};svgedit.units.init=function(e){s=e;e=document.createElementNS("http://www.w3.org/2000/svg","svg");document.body.appendChild(e);var f=document.createElementNS("http://www.w3.org/2000/svg","rect");f.setAttribute("width","1em");f.setAttribute("height","1ex");f.setAttribute("x","1in");e.appendChild(f);f=f.getBBox();document.body.removeChild(e); -e=f.x;v.em=f.width;v.ex=f.height;v["in"]=e;v.cm=e/2.54;v.mm=e/25.4;v.pt=e/72;v.pc=e/6;v["%"]=0};svgedit.units.getTypeMap=function(){return v};svgedit.units.shortFloat=function(e){var f=s.getRoundDigits();if(isNaN(e)){if($.isArray(e))return svgedit.units.shortFloat(e[0])+","+svgedit.units.shortFloat(e[1])}else return+(+e).toFixed(f);return parseFloat(e).toFixed(f)-0};svgedit.units.convertUnit=function(e,f){f=f||s.getBaseUnit();return svgedit.unit.shortFloat(e/v[f])};svgedit.units.setUnitAttr=function(e, -f,k){isNaN(k)||e.getAttribute(f);e.setAttribute(f,k)};var G={line:["x1","x2","y1","y2"],circle:["cx","cy","r"],ellipse:["cx","cy","rx","ry"],foreignObject:["x","y","width","height"],rect:["x","y","width","height"],image:["x","y","width","height"],use:["x","y","width","height"],text:["x","y"]};svgedit.units.convertAttrs=function(e){var f=e.tagName,k=s.getBaseUnit();if(f=G[f])for(var n=f.length,F=0;F<n;F++){var B=f[F],A=e.getAttribute(B);if(A)isNaN(A)||e.setAttribute(B,A/v[k]+k)}};svgedit.units.convertToNum= -function(e,f){if(!isNaN(f))return f-0;if(f.substr(-1)==="%"){var k=f.substr(0,f.length-1)/100,n=s.getWidth(),F=s.getHeight();return a.indexOf(e)>=0?k*n:J.indexOf(e)>=0?k*F:k*Math.sqrt(n*n+F*F)/Math.sqrt(2)}else{n=f.substr(-2);k=f.substr(0,f.length-2);return k*v[n]}};svgedit.units.isValidUnit=function(e,f,k){var n=false;if(l.indexOf(e)>=0)if(isNaN(f)){f=f.toLowerCase();$.each(v,function(A){if(!n)if(RegExp("^-?[\\d\\.]+"+A+"$").test(f))n=true})}else n=true;else if(e=="id"){e=false;try{var F=s.getElement(f); -e=F==null||F===k}catch(B){}return e}else n=true;return n}})();svgedit=svgedit||{}; -(function(){function a(e){if(svgedit.browser.supportsHVLineContainerBBox())try{return e.getBBox()}catch(f){}var k=$.data(e,"ref"),n=null;if(k){var F=$(k).children().clone().attr("visibility","hidden");$(G).append(F);n=F.filter("line, path")}else n=$(e).find("line, path");var B=false;if(n.length){n.each(function(){var A=this.getBBox();if(!A.width||!A.height)B=true});if(B){e=k?F:$(e).children();ret=getStrokedBBox(e)}else ret=e.getBBox()}else ret=e.getBBox();k&&F.remove();return ret}if(!svgedit.utilities)svgedit.utilities= -{};var J="a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use".split(","),l=null,s=null,v=null,G=null;svgedit.utilities.init=function(e){l=e;s=e.getDOMDocument();v=e.getDOMContainer();G=e.getSVGRoot()};svgedit.utilities.toXml=function(e){return $("<p/>").text(e).html()};svgedit.utilities.fromXml=function(e){return $("<p/>").html(e).text()};svgedit.utilities.encode64=function(e){e=svgedit.utilities.convertToXMLReferences(e);if(window.btoa)return window.btoa(e); -var f=Array(Math.floor((e.length+2)/3)*4),k,n,F,B,A,O,Z=0,N=0;do{k=e.charCodeAt(Z++);n=e.charCodeAt(Z++);F=e.charCodeAt(Z++);B=k>>2;k=(k&3)<<4|n>>4;A=(n&15)<<2|F>>6;O=F&63;if(isNaN(n))A=O=64;else if(isNaN(F))O=64;f[N++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(B);f[N++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(k);f[N++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(A);f[N++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(O)}while(Z< -e.length);return f.join("")};svgedit.utilities.decode64=function(e){if(window.atob)return window.atob(e);var f="",k,n,F="",B,A="",O=0;e=e.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{k="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(e.charAt(O++));n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(e.charAt(O++));B="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(e.charAt(O++));A="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(e.charAt(O++)); -k=k<<2|n>>4;n=(n&15)<<4|B>>2;F=(B&3)<<6|A;f+=String.fromCharCode(k);if(B!=64)f+=String.fromCharCode(n);if(A!=64)f+=String.fromCharCode(F)}while(O<e.length);return unescape(f)};svgedit.utilities.convertToXMLReferences=function(e){for(var f="",k=0;k<e.length;k++){var n=e.charCodeAt(k);if(n<128)f+=e[k];else if(n>127)f+="&#"+n+";"}return f};svgedit.utilities.text2xml=function(e){if(e.indexOf("<svg:svg")>=0)e=e.replace(/<(\/?)svg:/g,"<$1").replace("xmlns:svg","xmlns");var f;try{var k=window.DOMParser? -new DOMParser:new ActiveXObject("Microsoft.XMLDOM");k.async=false}catch(n){throw Error("XML Parser could not be instantiated");}try{f=k.loadXML?k.loadXML(e)?k:false:k.parseFromString(e,"text/xml")}catch(F){throw Error("Error parsing XML string");}return f};svgedit.utilities.bboxToObj=function(e){return{x:e.x,y:e.y,width:e.width,height:e.height}};svgedit.utilities.walkTree=function(e,f){if(e&&e.nodeType==1){f(e);for(var k=e.childNodes.length;k--;)svgedit.utilities.walkTree(e.childNodes.item(k),f)}}; -svgedit.utilities.walkTreePost=function(e,f){if(e&&e.nodeType==1){for(var k=e.childNodes.length;k--;)svgedit.utilities.walkTree(e.childNodes.item(k),f);f(e)}};svgedit.utilities.getUrlFromAttr=function(e){if(e)if(e.indexOf('url("')===0)return e.substring(5,e.indexOf('"',6));else if(e.indexOf("url('")===0)return e.substring(5,e.indexOf("'",6));else if(e.indexOf("url(")===0)return e.substring(4,e.indexOf(")"));return null};svgedit.utilities.getHref=function(e){return e.getAttributeNS("http://www.w3.org/1999/xlink", -"href")};svgedit.utilities.setHref=function(e,f){e.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",f)};svgedit.utilities.findDefs=function(e){e=l.getSVGContent().documentElement;var f=e.getElementsByTagNameNS("http://www.w3.org/2000/svg","defs");return f=f.length>0?f[0]:e.insertBefore(e.ownerDocument.createElementNS("http://www.w3.org/2000/svg","defs"),e.firstChild.nextSibling)};svgedit.utilities.getPathBBox=function(e){var f=e.pathSegList,k=f.numberOfItems;e=[[],[]];var n=f.getItem(0), -F=[n.x,n.y];for(n=0;n<k;n++){var B=f.getItem(n);if(typeof B.x!="undefined"){e[0].push(F[0]);e[1].push(F[1]);if(B.x1){for(var A=[B.x1,B.y1],O=[B.x2,B.y2],Z=[B.x,B.y],N=0;N<2;N++){B=function(da){return Math.pow(1-da,3)*F[N]+3*Math.pow(1-da,2)*da*A[N]+3*(1-da)*Math.pow(da,2)*O[N]+Math.pow(da,3)*Z[N]};var L=6*F[N]-12*A[N]+6*O[N],qa=-3*F[N]+9*A[N]-9*O[N]+3*Z[N],aa=3*A[N]-3*F[N];if(qa==0){if(L!=0){L=-aa/L;0<L&&L<1&&e[N].push(B(L))}}else{aa=Math.pow(L,2)-4*aa*qa;if(!(aa<0)){var ja=(-L+Math.sqrt(aa))/(2* -qa);0<ja&&ja<1&&e[N].push(B(ja));L=(-L-Math.sqrt(aa))/(2*qa);0<L&&L<1&&e[N].push(B(L))}}}F=Z}else{e[0].push(B.x);e[1].push(B.y)}}}f=Math.min.apply(null,e[0]);k=Math.max.apply(null,e[0])-f;n=Math.min.apply(null,e[1]);e=Math.max.apply(null,e[1])-n;return{x:f,y:n,width:k,height:e}};svgedit.utilities.getBBox=function(e){var f=e||l.geSelectedElements()[0];if(e.nodeType!=1)return null;e=null;var k=f.nodeName;switch(k){case "text":if(f.textContent===""){f.textContent="a";e=f.getBBox();f.textContent=""}else try{e= -f.getBBox()}catch(n){}break;case "path":if(svgedit.browser.supportsPathBBox())try{e=f.getBBox()}catch(F){}else e=svgedit.utilities.getPathBBox(f);break;case "g":case "a":e=a(f);break;default:if(k==="use")e=a(f,true);if(k==="use"){e||(e=f.getBBox());k={};k.width=e.width;k.height=e.height;k.x=e.x+parseFloat(f.getAttribute("x")||0);k.y=e.y+parseFloat(f.getAttribute("y")||0);e=k}else if(~J.indexOf(k))try{e=f.getBBox()}catch(B){f=$(f).closest("foreignObject");if(f.length)try{e=f[0].getBBox()}catch(A){e= -null}else e=null}}if(e)e=svgedit.utilities.bboxToObj(e);return e};svgedit.utilities.getRotationAngle=function(e,f){var k=e||l.getSelectedElements()[0];k=svgedit.transformlist.getTransformList(k);if(!k)return 0;for(var n=k.numberOfItems,F=0;F<n;++F){var B=k.getItem(F);if(B.type==4)return f?B.angle*Math.PI/180:B.angle}return 0};svgedit.utilities.getElem=svgedit.browser.supportsSelectors()?function(e){return G.querySelector("#"+e)}:svgedit.browser.supportsXpath()?function(e){return s.evaluate('svg:svg[@id="svgroot"]//svg:*[@id="'+ -e+'"]',v,function(){return"http://www.w3.org/2000/svg"},9,null).singleNodeValue}:function(e){return $(G).find("[id="+e+"]")[0]};svgedit.utilities.assignAttributes=function(e,f,k,n){k||(k=0);svgedit.browser.isOpera()||G.suspendRedraw(k);for(var F in f)if(k=F.substr(0,4)==="xml:"?"http://www.w3.org/XML/1998/namespace":F.substr(0,6)==="xlink:"?"http://www.w3.org/1999/xlink":null)e.setAttributeNS(k,F,f[F]);else n?svgedit.units.setUnitAttr(e,F,f[F]):e.setAttribute(F,f[F]);svgedit.browser.isOpera()||G.unsuspendRedraw(null)}; -svgedit.utilities.cleanupElement=function(e){var f=G.suspendRedraw(60),k={"fill-opacity":1,"stop-opacity":1,opacity:1,stroke:"none","stroke-dasharray":"none","stroke-linejoin":"miter","stroke-linecap":"butt","stroke-opacity":1,"stroke-width":1,rx:0,ry:0},n;for(n in k){var F=k[n];e.getAttribute(n)==F&&e.removeAttribute(n)}G.unsuspendRedraw(f)}})();svgedit=svgedit||{}; -(function(){if(!svgedit.sanitize)svgedit.sanitize={};var a={};a["http://www.w3.org/1999/xlink"]="xlink";a["http://www.w3.org/XML/1998/namespace"]="xml";a["http://www.w3.org/2000/xmlns/"]="xmlns";a["http://svg-edit.googlecode.com"]="se";a["http://www.w3.org/1999/xhtml"]="xhtml";a["http://www.w3.org/1998/Math/MathML"]="mathml";var J={};$.each(a,function(v,G){J[G]=v});var l={a:["class","clip-path","clip-rule","fill","fill-opacity","fill-rule","filter","id","mask","opacity","stroke","stroke-dasharray", -"stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","transform","xlink:href","xlink:title"],circle:["class","clip-path","clip-rule","cx","cy","fill","fill-opacity","fill-rule","filter","id","mask","opacity","r","requiredFeatures","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","transform"],clipPath:["class", -"clipPathUnits","id"],defs:[],style:["type"],desc:[],ellipse:["class","clip-path","clip-rule","cx","cy","fill","fill-opacity","fill-rule","filter","id","mask","opacity","requiredFeatures","rx","ry","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","transform"],feGaussianBlur:["class","color-interpolation-filters","id","requiredFeatures","stdDeviation"],filter:["class","color-interpolation-filters", -"filterRes","filterUnits","height","id","primitiveUnits","requiredFeatures","width","x","xlink:href","y"],foreignObject:["class","font-size","height","id","opacity","requiredFeatures","style","transform","width","x","y"],g:["class","clip-path","clip-rule","id","display","fill","fill-opacity","fill-rule","filter","mask","opacity","requiredFeatures","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage", -"transform","font-family","font-size","font-style","font-weight","text-anchor"],image:["class","clip-path","clip-rule","filter","height","id","mask","opacity","requiredFeatures","style","systemLanguage","transform","width","x","xlink:href","xlink:title","y"],line:["class","clip-path","clip-rule","fill","fill-opacity","fill-rule","filter","id","marker-end","marker-mid","marker-start","mask","opacity","requiredFeatures","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin", -"stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","transform","x1","x2","y1","y2"],linearGradient:["class","id","gradientTransform","gradientUnits","requiredFeatures","spreadMethod","systemLanguage","x1","x2","xlink:href","y1","y2"],marker:["id","class","markerHeight","markerUnits","markerWidth","orient","preserveAspectRatio","refX","refY","systemLanguage","viewBox"],mask:["class","height","id","maskContentUnits","maskUnits","width","x","y"],metadata:["class","id"],path:["class", -"clip-path","clip-rule","d","fill","fill-opacity","fill-rule","filter","id","marker-end","marker-mid","marker-start","mask","opacity","requiredFeatures","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","transform"],pattern:["class","height","id","patternContentUnits","patternTransform","patternUnits","requiredFeatures","style","systemLanguage","viewBox","width","x","xlink:href","y"],polygon:["class", -"clip-path","clip-rule","id","fill","fill-opacity","fill-rule","filter","id","class","marker-end","marker-mid","marker-start","mask","opacity","points","requiredFeatures","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","transform"],polyline:["class","clip-path","clip-rule","id","fill","fill-opacity","fill-rule","filter","marker-end","marker-mid","marker-start","mask","opacity","points", -"requiredFeatures","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","transform"],radialGradient:["class","cx","cy","fx","fy","gradientTransform","gradientUnits","id","r","requiredFeatures","spreadMethod","systemLanguage","xlink:href"],rect:["class","clip-path","clip-rule","fill","fill-opacity","fill-rule","filter","height","id","mask","opacity","requiredFeatures","rx","ry","stroke","stroke-dasharray", -"stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","transform","width","x","y"],stop:["class","id","offset","requiredFeatures","stop-color","stop-opacity","style","systemLanguage"],svg:["class","clip-path","clip-rule","filter","id","height","mask","preserveAspectRatio","requiredFeatures","style","systemLanguage","viewBox","width","x","xmlns","xmlns:se","xmlns:xlink","y"],"switch":["class","id","requiredFeatures","systemLanguage"], -symbol:["class","fill","fill-opacity","fill-rule","filter","font-family","font-size","font-style","font-weight","id","opacity","preserveAspectRatio","requiredFeatures","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","transform","viewBox"],text:["class","clip-path","clip-rule","fill","fill-opacity","fill-rule","filter","font-family","font-size","font-style","font-weight","id","mask","opacity", -"requiredFeatures","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","text-anchor","transform","x","xml:space","y"],textPath:["class","id","method","requiredFeatures","spacing","startOffset","style","systemLanguage","transform","xlink:href"],title:[],tspan:["class","clip-path","clip-rule","dx","dy","fill","fill-opacity","fill-rule","filter","font-family","font-size","font-style","font-weight", -"id","mask","opacity","requiredFeatures","rotate","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","style","systemLanguage","text-anchor","textLength","transform","x","xml:space","y"],use:["class","clip-path","clip-rule","fill","fill-opacity","fill-rule","filter","height","id","mask","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width", -"style","transform","width","x","xlink:href","y"],annotation:["encoding"],"annotation-xml":["encoding"],maction:["actiontype","other","selection"],math:["class","id","display","xmlns"],menclose:["notation"],merror:[],mfrac:["linethickness"],mi:["mathvariant"],mmultiscripts:[],mn:[],mo:["fence","lspace","maxsize","minsize","rspace","stretchy"],mover:[],mpadded:["lspace","width","height","depth","voffset"],mphantom:[],mprescripts:[],mroot:[],mrow:["xlink:href","xlink:type","xmlns:xlink"],mspace:["depth", -"height","width"],msqrt:[],mstyle:["displaystyle","mathbackground","mathcolor","mathvariant","scriptlevel"],msub:[],msubsup:[],msup:[],mtable:["align","columnalign","columnlines","columnspacing","displaystyle","equalcolumns","equalrows","frame","rowalign","rowlines","rowspacing","width"],mtd:["columnalign","columnspan","rowalign","rowspan"],mtext:[],mtr:["columnalign","rowalign"],munder:[],munderover:[],none:[],semantics:[]},s={};$.each(l,function(v,G){var e={};$.each(G,function(f,k){if(k.indexOf(":")>= -0){var n=k.split(":");e[n[1]]=J[n[0]]}else e[k]=k=="xmlns"?"http://www.w3.org/2000/xmlns/":null});s[v]=e});svgedit.sanitize.getNSMap=function(){return a};svgedit.sanitize.sanitizeSvg=function(v){if(v.nodeType==3){v.nodeValue=v.nodeValue.replace(/^\s+|\s+$/g,"");v.nodeValue.length||v.parentNode.removeChild(v)}if(v.nodeType==1){var G=v.parentNode;if(v.ownerDocument&&G){var e=l[v.nodeName],f=s[v.nodeName];if(e!=undefined){for(var k=[],n=v.attributes.length;n--;){var F=v.attributes.item(n),B=F.nodeName, -A=F.localName,O=F.namespaceURI;if(!(f.hasOwnProperty(A)&&O==f[A]&&O!="http://www.w3.org/2000/xmlns/")&&!(O=="http://www.w3.org/2000/xmlns/"&&a[F.nodeValue])){B.indexOf("se:")==0&&k.push([B,F.nodeValue]);v.removeAttributeNS(O,A)}if(svgedit.browser.isGecko())switch(B){case "transform":case "gradientTransform":case "patternTransform":A=F.nodeValue.replace(/(\d)-/g,"$1 -");v.setAttribute(B,A)}if(B=="style"){F=F.nodeValue.split(";");for(B=F.length;B--;){A=F[B].split(":");e.indexOf(A[0])>=0&&v.setAttribute(A[0], -A[1])}v.removeAttribute("style")}}$.each(k,function(Z,N){v.setAttributeNS("http://svg-edit.googlecode.com",N[0],N[1])});if((n=svgedit.utilities.getHref(v))&&["filter","linearGradient","pattern","radialGradient","textPath","use"].indexOf(v.nodeName)>=0)if(n[0]!="#"){svgedit.utilities.setHref(v,"");v.removeAttributeNS("http://www.w3.org/1999/xlink","href")}if(v.nodeName=="use"&&!svgedit.utilities.getHref(v))G.removeChild(v);else{$.each(["clip-path","fill","filter","marker-end","marker-mid","marker-start", -"mask","stroke"],function(Z,N){var L=v.getAttribute(N);if(L)if((L=svgedit.utilities.getUrlFromAttr(L))&&L[0]!=="#"){v.setAttribute(N,"");v.removeAttribute(N)}});for(n=v.childNodes.length;n--;)svgedit.sanitize.sanitizeSvg(v.childNodes.item(n))}}else{for(e=[];v.hasChildNodes();)e.push(G.insertBefore(v.firstChild,v));G.removeChild(v);for(n=e.length;n--;)svgedit.sanitize.sanitizeSvg(e[n])}}}}})();svgedit=svgedit||{}; -(function(){if(!svgedit.history)svgedit.history={};svgedit.history.HistoryEventTypes={BEFORE_APPLY:"before_apply",AFTER_APPLY:"after_apply",BEFORE_UNAPPLY:"before_unapply",AFTER_UNAPPLY:"after_unapply"};svgedit.history.MoveElementCommand=function(a,J,l,s){this.elem=a;this.text=s?"Move "+a.tagName+" to "+s:"Move "+a.tagName;this.oldNextSibling=J;this.oldParent=l;this.newNextSibling=a.nextSibling;this.newParent=a.parentNode};svgedit.history.MoveElementCommand.type=function(){return"svgedit.history.MoveElementCommand"};svgedit.history.MoveElementCommand.prototype.type= -svgedit.history.MoveElementCommand.type;svgedit.history.MoveElementCommand.prototype.getText=function(){return this.text};svgedit.history.MoveElementCommand.prototype.apply=function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY,this);this.elem=this.newParent.insertBefore(this.elem,this.newNextSibling);a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY,this)};svgedit.history.MoveElementCommand.prototype.unapply=function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, -this);this.elem=this.oldParent.insertBefore(this.elem,this.oldNextSibling);a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY,this)};svgedit.history.MoveElementCommand.prototype.elements=function(){return[this.elem]};svgedit.history.InsertElementCommand=function(a,J){this.elem=a;this.text=J||"Create "+a.tagName;this.parent=a.parentNode;this.nextSibling=this.elem.nextSibling};svgedit.history.InsertElementCommand.type=function(){return"svgedit.history.InsertElementCommand"};svgedit.history.InsertElementCommand.prototype.type= -svgedit.history.InsertElementCommand.type;svgedit.history.InsertElementCommand.prototype.getText=function(){return this.text};svgedit.history.InsertElementCommand.prototype.apply=function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY,this);this.elem=this.parent.insertBefore(this.elem,this.nextSibling);a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY,this)};svgedit.history.InsertElementCommand.prototype.unapply=function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, -this);this.parent=this.elem.parentNode;this.elem=this.elem.parentNode.removeChild(this.elem);a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY,this)};svgedit.history.InsertElementCommand.prototype.elements=function(){return[this.elem]};svgedit.history.RemoveElementCommand=function(a,J,l,s){this.elem=a;this.text=s||"Delete "+a.tagName;this.nextSibling=J;this.parent=l;svgedit.transformlist.removeElementFromListMap(a)};svgedit.history.RemoveElementCommand.type=function(){return"svgedit.history.RemoveElementCommand"}; -svgedit.history.RemoveElementCommand.prototype.type=svgedit.history.RemoveElementCommand.type;svgedit.history.RemoveElementCommand.prototype.getText=function(){return this.text};svgedit.history.RemoveElementCommand.prototype.apply=function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY,this);svgedit.transformlist.removeElementFromListMap(this.elem);this.parent=this.elem.parentNode;this.elem=this.parent.removeChild(this.elem);a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, -this)};svgedit.history.RemoveElementCommand.prototype.unapply=function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY,this);svgedit.transformlist.removeElementFromListMap(this.elem);this.nextSibling==null&&window.console&&console.log("Error: reference element was lost");this.parent.insertBefore(this.elem,this.nextSibling);a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY,this)};svgedit.history.RemoveElementCommand.prototype.elements=function(){return[this.elem]}; -svgedit.history.ChangeElementCommand=function(a,J,l){this.elem=a;this.text=l?"Change "+a.tagName+" "+l:"Change "+a.tagName;this.newValues={};this.oldValues=J;for(var s in J)this.newValues[s]=s=="#text"?a.textContent:s=="#href"?svgedit.utilities.getHref(a):a.getAttribute(s)};svgedit.history.ChangeElementCommand.type=function(){return"svgedit.history.ChangeElementCommand"};svgedit.history.ChangeElementCommand.prototype.type=svgedit.history.ChangeElementCommand.type;svgedit.history.ChangeElementCommand.prototype.getText= -function(){return this.text};svgedit.history.ChangeElementCommand.prototype.apply=function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY,this);var J=false,l;for(l in this.newValues){if(this.newValues[l])if(l=="#text")this.elem.textContent=this.newValues[l];else l=="#href"?svgedit.utilities.setHref(this.elem,this.newValues[l]):this.elem.setAttribute(l,this.newValues[l]);else if(l=="#text")this.elem.textContent="";else{this.elem.setAttribute(l,"");this.elem.removeAttribute(l)}if(l== -"transform")J=true}if(!J)if(J=svgedit.utilities.getRotationAngle(this.elem)){l=elem.getBBox();J=["rotate(",J," ",l.x+l.width/2,",",l.y+l.height/2,")"].join("");J!=elem.getAttribute("transform")&&elem.setAttribute("transform",J)}a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY,this);return true};svgedit.history.ChangeElementCommand.prototype.unapply=function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY,this);var J=false,l;for(l in this.oldValues){if(this.oldValues[l])if(l== -"#text")this.elem.textContent=this.oldValues[l];else l=="#href"?svgedit.utilities.setHref(this.elem,this.oldValues[l]):this.elem.setAttribute(l,this.oldValues[l]);else if(l=="#text")this.elem.textContent="";else this.elem.removeAttribute(l);if(l=="transform")J=true}if(!J)if(J=svgedit.utilities.getRotationAngle(this.elem)){l=elem.getBBox();J=["rotate(",J," ",l.x+l.width/2,",",l.y+l.height/2,")"].join("");J!=elem.getAttribute("transform")&&elem.setAttribute("transform",J)}svgedit.transformlist.removeElementFromListMap(this.elem); -a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY,this);return true};svgedit.history.ChangeElementCommand.prototype.elements=function(){return[this.elem]};svgedit.history.BatchCommand=function(a){this.text=a||"Batch Command";this.stack=[]};svgedit.history.BatchCommand.type=function(){return"svgedit.history.BatchCommand"};svgedit.history.BatchCommand.prototype.type=svgedit.history.BatchCommand.type;svgedit.history.BatchCommand.prototype.getText=function(){return this.text};svgedit.history.BatchCommand.prototype.apply= -function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY,this);for(var J=this.stack.length,l=0;l<J;++l)this.stack[l].apply(a);a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY,this)};svgedit.history.BatchCommand.prototype.unapply=function(a){a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY,this);for(var J=this.stack.length-1;J>=0;J--)this.stack[J].unapply(a);a&&a.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, -this)};svgedit.history.BatchCommand.prototype.elements=function(){for(var a=[],J=this.stack.length;J--;)for(var l=this.stack[J].elements(),s=l.length;s--;)a.indexOf(l[s])==-1&&a.push(l[s]);return a};svgedit.history.BatchCommand.prototype.addSubCommand=function(a){this.stack.push(a)};svgedit.history.BatchCommand.prototype.isEmpty=function(){return this.stack.length==0};svgedit.history.UndoManager=function(a){this.handler_=a||null;this.undoStackPointer=0;this.undoStack=[];this.undoChangeStackPointer= --1;this.undoableChangeStack=[]};svgedit.history.UndoManager.prototype.resetUndoStack=function(){this.undoStack=[];this.undoStackPointer=0};svgedit.history.UndoManager.prototype.getUndoStackSize=function(){return this.undoStackPointer};svgedit.history.UndoManager.prototype.getRedoStackSize=function(){return this.undoStack.length-this.undoStackPointer};svgedit.history.UndoManager.prototype.getNextUndoCommandText=function(){return this.undoStackPointer>0?this.undoStack[this.undoStackPointer-1].getText(): -""};svgedit.history.UndoManager.prototype.getNextRedoCommandText=function(){return this.undoStackPointer<this.undoStack.length?this.undoStack[this.undoStackPointer].getText():""};svgedit.history.UndoManager.prototype.undo=function(){this.undoStackPointer>0&&this.undoStack[--this.undoStackPointer].unapply(this.handler_)};svgedit.history.UndoManager.prototype.redo=function(){this.undoStackPointer<this.undoStack.length&&this.undoStack.length>0&&this.undoStack[this.undoStackPointer++].apply(this.handler_)}; -svgedit.history.UndoManager.prototype.addCommandToHistory=function(a){if(this.undoStackPointer<this.undoStack.length&&this.undoStack.length>0)this.undoStack=this.undoStack.splice(0,this.undoStackPointer);this.undoStack.push(a);this.undoStackPointer=this.undoStack.length};svgedit.history.UndoManager.prototype.beginUndoableChange=function(a,J){for(var l=++this.undoChangeStackPointer,s=J.length,v=Array(s),G=Array(s);s--;){var e=J[s];if(e!=null){G[s]=e;v[s]=e.getAttribute(a)}}this.undoableChangeStack[l]= -{attrName:a,oldValues:v,elements:G}};svgedit.history.UndoManager.prototype.finishUndoableChange=function(){for(var a=this.undoChangeStackPointer--,J=this.undoableChangeStack[a],l=J.elements.length,s=J.attrName,v=new svgedit.history.BatchCommand("Change "+s);l--;){var G=J.elements[l];if(G!=null){var e={};e[s]=J.oldValues[l];e[s]!=G.getAttribute(s)&&v.addSubCommand(new svgedit.history.ChangeElementCommand(G,e,s))}}this.undoableChangeStack[a]=null;return v}})();svgedit=svgedit||{}; -(function(){if(!svgedit.select)svgedit.select={};var a,J,l,s;svgedit.browser.isTouch()?s=10:s=4;svgedit.select.Selector=function(v,G){this.id=v;this.selectedElement=G;this.locked=true;this.selectorGroup=a.createSVGElement({element:"g",attr:{id:"selectorGroup"+this.id}});this.selectorRect=this.selectorGroup.appendChild(a.createSVGElement({element:"path",attr:{id:"selectedBox"+this.id,fill:"none",stroke:"#22C","stroke-width":"1","stroke-dasharray":"5,5",style:"pointer-events:none"}}));this.gripCoords= -{nw:null,n:null,ne:null,e:null,se:null,s:null,sw:null,w:null};this.reset(this.selectedElement)};svgedit.select.Selector.prototype.reset=function(v){this.locked=true;this.selectedElement=v;this.resize();this.selectorGroup.setAttribute("display","inline")};svgedit.select.Selector.prototype.updateGripCursors=function(v){var G=[];v=Math.round(v/45);if(v<0)v+=8;for(var e in l.selectorGrips)G.push(e);for(;v>0;){G.push(G.shift());v--}v=0;for(e in l.selectorGrips){l.selectorGrips[e].setAttribute("style", -"cursor:"+G[v]+"-resize");v++}};svgedit.select.Selector.prototype.showGrips=function(v){l.selectorGripsGroup.setAttribute("display",v?"inline":"none");var G=this.selectedElement;this.hasGrips=v;if(G&&v){this.selectorGroup.appendChild(l.selectorGripsGroup);this.updateGripCursors(svgedit.utilities.getRotationAngle(G))}};svgedit.select.Selector.prototype.resize=function(){var v=this.selectorRect,G=l,e=G.selectorGrips,f=this.selectedElement,k=f.getAttribute("stroke-width"),n=a.currentZoom(),F=1/n;if(f.getAttribute("stroke")!== -"none"&&!isNaN(k))F+=k/2;var B=f.tagName;if(B==="text")F+=2/n;k=svgedit.transformlist.getTransformList(f);k=svgedit.math.transformListToTransform(k).matrix;k.e*=n;k.f*=n;var A=svgedit.utilities.getBBox(f);if(B==="g"&&!$.data(f,"gsvg"))if(B=a.getStrokedBBox(f.childNodes))A=B;B=A.x;var O=A.y,Z=A.width;A=A.height;F*=n;n=svgedit.math.transformBox(B*n,O*n,Z*n,A*n,k);k=n.aabox;B=k.x-F;O=k.y-F;Z=k.width+F*2;var N=k.height+F*2;k=B+Z/2;A=O+N/2;if(f=svgedit.utilities.getRotationAngle(f)){B=a.svgRoot().createSVGTransform(); -B.setRotate(-f,k,A);B=B.matrix;n.tl=svgedit.math.transformPoint(n.tl.x,n.tl.y,B);n.tr=svgedit.math.transformPoint(n.tr.x,n.tr.y,B);n.bl=svgedit.math.transformPoint(n.bl.x,n.bl.y,B);n.br=svgedit.math.transformPoint(n.br.x,n.br.y,B);B=n.tl;Z=B.x;N=B.y;var L=B.x,qa=B.y;B=Math.min;O=Math.max;Z=B(Z,B(n.tr.x,B(n.bl.x,n.br.x)))-F;N=B(N,B(n.tr.y,B(n.bl.y,n.br.y)))-F;L=O(L,O(n.tr.x,O(n.bl.x,n.br.x)))+F;qa=O(qa,O(n.tr.y,O(n.bl.y,n.br.y)))+F;B=Z;O=N;Z=L-Z;N=qa-N}F=a.svgRoot().suspendRedraw(100);v.setAttribute("d", -"M"+B+","+O+" L"+(B+Z)+","+O+" "+(B+Z)+","+(O+N)+" "+B+","+(O+N)+"z");this.selectorGroup.setAttribute("transform",f?"rotate("+[f,k,A].join(",")+")":"");this.gripCoords={nw:[B,O],ne:[B+Z,O],sw:[B,O+N],se:[B+Z,O+N],n:[B+Z/2,O],w:[B,O+N/2],e:[B+Z,O+N/2],s:[B+Z/2,O+N]};for(var aa in this.gripCoords){v=this.gripCoords[aa];e[aa].setAttribute("cx",v[0]);e[aa].setAttribute("cy",v[1])}G.rotateGripConnector.setAttribute("x1",B+Z/2);G.rotateGripConnector.setAttribute("y1",O);G.rotateGripConnector.setAttribute("x2", -B+Z/2);G.rotateGripConnector.setAttribute("y2",O-s*5);G.rotateGrip.setAttribute("cx",B+Z/2);G.rotateGrip.setAttribute("cy",O-s*5);a.svgRoot().unsuspendRedraw(F)};svgedit.select.SelectorManager=function(){this.rubberBandBox=this.selectorParentGroup=null;this.selectors=[];this.selectorMap={};this.selectorGrips={nw:null,n:null,ne:null,e:null,se:null,s:null,sw:null,w:null};this.rotateGrip=this.rotateGripConnector=this.selectorGripsGroup=null;this.initGroup()};svgedit.select.SelectorManager.prototype.initGroup= -function(){this.selectorParentGroup&&this.selectorParentGroup.parentNode&&this.selectorParentGroup.parentNode.removeChild(this.selectorParentGroup);this.selectorParentGroup=a.createSVGElement({element:"g",attr:{id:"selectorParentGroup"}});this.selectorGripsGroup=a.createSVGElement({element:"g",attr:{display:"none"}});this.selectorParentGroup.appendChild(this.selectorGripsGroup);a.svgRoot().appendChild(this.selectorParentGroup);this.selectorMap={};this.selectors=[];this.rubberBandBox=null;for(var v in this.selectorGrips){var G= -a.createSVGElement({element:"circle",attr:{id:"selectorGrip_resize_"+v,fill:"#22C",r:s,style:"cursor:"+v+"-resize","stroke-width":2,"pointer-events":"all"}});$.data(G,"dir",v);$.data(G,"type","resize");this.selectorGrips[v]=this.selectorGripsGroup.appendChild(G)}this.rotateGripConnector=this.selectorGripsGroup.appendChild(a.createSVGElement({element:"line",attr:{id:"selectorGrip_rotateconnector",stroke:"#22C","stroke-width":"1"}}));this.rotateGrip=this.selectorGripsGroup.appendChild(a.createSVGElement({element:"circle", -attr:{id:"selectorGrip_rotate",fill:"lime",r:s,stroke:"#22C","stroke-width":2,style:"cursor:url("+J.imgPath+"rotate.png) 12 12, auto;"}}));$.data(this.rotateGrip,"type","rotate");if(!$("#canvasBackground").length){v=J.dimensions;v=a.createSVGElement({element:"svg",attr:{id:"canvasBackground",width:v[0],height:v[1],x:0,y:0,overflow:svgedit.browser.isWebkit()?"none":"visible",style:"pointer-events:none"}});G=a.createSVGElement({element:"rect",attr:{width:"100%",height:"100%",x:0,y:0,"stroke-width":1, -stroke:"#000",fill:"#FFF",style:"pointer-events:none"}});v.appendChild(G);a.svgRoot().insertBefore(v,a.svgContent())}};svgedit.select.SelectorManager.prototype.requestSelector=function(v){if(v==null)return null;var G=this.selectors.length;if(typeof this.selectorMap[v.id]=="object"){this.selectorMap[v.id].locked=true;return this.selectorMap[v.id]}for(var e=0;e<G;++e)if(this.selectors[e]&&!this.selectors[e].locked){this.selectors[e].locked=true;this.selectors[e].reset(v);this.selectorMap[v.id]=this.selectors[e]; -return this.selectors[e]}this.selectors[G]=new svgedit.select.Selector(G,v);this.selectorParentGroup.appendChild(this.selectors[G].selectorGroup);this.selectorMap[v.id]=this.selectors[G];return this.selectors[G]};svgedit.select.SelectorManager.prototype.releaseSelector=function(v){if(v!=null)for(var G=this.selectors.length,e=this.selectorMap[v.id],f=0;f<G;++f)if(this.selectors[f]&&this.selectors[f]==e){e.locked==false&&console.log("WARNING! selector was released but was already unlocked");delete this.selectorMap[v.id]; -e.locked=false;e.selectedElement=null;e.showGrips(false);try{e.selectorGroup.setAttribute("display","none")}catch(k){}break}};svgedit.select.SelectorManager.prototype.getRubberBandBox=function(){if(!this.rubberBandBox)this.rubberBandBox=this.selectorParentGroup.appendChild(a.createSVGElement({element:"rect",attr:{id:"selectorRubberBand",fill:"#22C","fill-opacity":0.15,stroke:"#22C","stroke-width":0.5,display:"none",style:"pointer-events:none"}}));return this.rubberBandBox};svgedit.select.init=function(v, -G){J=v;a=G;l=new svgedit.select.SelectorManager};svgedit.select.getSelectorManager=function(){return l}})();svgedit=svgedit||{}; -(function(){if(!svgedit.draw)svgedit.draw={};var a="a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use".split(","),J={LET_DOCUMENT_DECIDE:0,ALWAYS_RANDOMIZE:1,NEVER_RANDOMIZE:2},l=J.LET_DOCUMENT_DECIDE;svgedit.draw.Layer=function(s,v){this.name_=s;this.group_=v};svgedit.draw.Layer.prototype.getName=function(){return this.name_};svgedit.draw.Layer.prototype.getGroup=function(){return this.group_};svgedit.draw.randomizeIds=function(s,v){l=s==false?J.NEVER_RANDOMIZE: -J.ALWAYS_RANDOMIZE;if(l==J.ALWAYS_RANDOMIZE&&!v.getNonce())v.setNonce(Math.floor(Math.random()*100001));else l==J.NEVER_RANDOMIZE&&v.getNonce()&&v.clearNonce()};svgedit.draw.Drawing=function(s,v){if(!s||!s.tagName||!s.namespaceURI||s.tagName!="svg"||s.namespaceURI!="http://www.w3.org/2000/svg")throw"Error: svgedit.draw.Drawing instance initialized without a <svg> element";this.svgElem_=s;this.obj_num=0;this.idPrefix=v||"svg_";this.releasedNums=[];this.all_layers=[];this.current_layer=null;this.nonce_= -"";var G=this.svgElem_.getAttributeNS("http://svg-edit.googlecode.com","nonce");if(G&&l!=J.NEVER_RANDOMIZE)this.nonce_=G;else l==J.ALWAYS_RANDOMIZE&&this.setNonce(Math.floor(Math.random()*100001))};svgedit.draw.Drawing.prototype.getElem_=function(s){return this.svgElem_.querySelector?this.svgElem_.querySelector("#"+s):$(this.svgElem_).find("[id="+s+"]")[0]};svgedit.draw.Drawing.prototype.getSvgElem=function(){return this.svgElem_};svgedit.draw.Drawing.prototype.getNonce=function(){return this.nonce_}; -svgedit.draw.Drawing.prototype.setNonce=function(s){this.svgElem_.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:se","http://svg-edit.googlecode.com");this.svgElem_.setAttributeNS("http://svg-edit.googlecode.com","se:nonce",s);this.nonce_=s};svgedit.draw.Drawing.prototype.clearNonce=function(){this.nonce_=""};svgedit.draw.Drawing.prototype.getId=function(){return this.nonce_?this.idPrefix+this.nonce_+"_"+this.obj_num:this.idPrefix+this.obj_num};svgedit.draw.Drawing.prototype.getNextId=function(){var s= -this.obj_num,v=false;if(this.releasedNums.length>0){this.obj_num=this.releasedNums.pop();v=true}else this.obj_num++;for(var G=this.getId();this.getElem_(G);){if(v){this.obj_num=s;v=false}this.obj_num++;G=this.getId()}if(v)this.obj_num=s;return G};svgedit.draw.Drawing.prototype.releaseId=function(s){var v=this.idPrefix+(this.nonce_?this.nonce_+"_":"");if(typeof s!="string"||s.indexOf(v)!=0)return false;s=parseInt(s.substr(v.length));if(typeof s!="number"||s<=0||this.releasedNums.indexOf(s)!=-1)return false; -this.releasedNums.push(s);return true};svgedit.draw.Drawing.prototype.getNumLayers=function(){return this.all_layers.length};svgedit.draw.Drawing.prototype.hasLayer=function(s){for(var v=0;v<this.getNumLayers();v++)if(this.all_layers[v][0]==s)return true;return false};svgedit.draw.Drawing.prototype.getLayerName=function(s){if(s>=0&&s<this.getNumLayers())return this.all_layers[s][0];return""};svgedit.draw.Drawing.prototype.getCurrentLayer=function(){return this.current_layer};svgedit.draw.Drawing.prototype.getCurrentLayerName= -function(){for(var s=0;s<this.getNumLayers();++s)if(this.all_layers[s][1]==this.current_layer)return this.getLayerName(s);return""};svgedit.draw.Drawing.prototype.setCurrentLayer=function(s){for(var v=0;v<this.getNumLayers();++v)if(s==this.getLayerName(v)){if(this.current_layer!=this.all_layers[v][1]){this.current_layer.setAttribute("style","pointer-events:none");this.current_layer=this.all_layers[v][1];this.current_layer.setAttribute("style","pointer-events:all")}return true}return false};svgedit.draw.Drawing.prototype.deleteCurrentLayer= -function(){if(this.current_layer&&this.getNumLayers()>1){var s=this.current_layer.parentNode.removeChild(this.current_layer);this.identifyLayers();return s}return null};svgedit.draw.Drawing.prototype.identifyLayers=function(){this.all_layers=[];for(var s=this.svgElem_.childNodes.length,v=[],G=[],e=null,f=false,k=0;k<s;++k){var n=this.svgElem_.childNodes.item(k);if(n&&n.nodeType==1)if(n.tagName=="g"){f=true;var F=$("title",n).text();if(!F&&svgedit.browser.isOpera()&&n.querySelectorAll)F=$(n.querySelectorAll("title")).text(); -if(F){G.push(F);this.all_layers.push([F,n]);e=n;svgedit.utilities.walkTree(n,function(B){B.setAttribute("style","pointer-events:inherit")});e.setAttribute("style","pointer-events:none")}else v.push(n)}else if(~a.indexOf(n.nodeName)){svgedit.utilities.getBBox(n);v.push(n)}}s=this.svgElem_.ownerDocument;if(v.length>0||!f){for(k=1;G.indexOf("Layer "+k)>=0;)k++;G="Layer "+k;e=s.createElementNS("http://www.w3.org/2000/svg","g");f=s.createElementNS("http://www.w3.org/2000/svg","title");f.textContent=G; -e.appendChild(f);for(f=0;f<v.length;++f)e.appendChild(v[f]);this.svgElem_.appendChild(e);this.all_layers.push([G,e])}svgedit.utilities.walkTree(e,function(B){B.setAttribute("style","pointer-events:inherit")});this.current_layer=e;this.current_layer.setAttribute("style","pointer-events:all")};svgedit.draw.Drawing.prototype.createLayer=function(s){var v=this.svgElem_.ownerDocument,G=v.createElementNS("http://www.w3.org/2000/svg","g");v=v.createElementNS("http://www.w3.org/2000/svg","title");v.textContent= -s;G.appendChild(v);this.svgElem_.appendChild(G);this.identifyLayers();return G};svgedit.draw.Drawing.prototype.getLayerVisibility=function(s){for(var v=null,G=0;G<this.getNumLayers();++G)if(this.getLayerName(G)==s){v=this.all_layers[G][1];break}if(!v)return false;return v.getAttribute("display")!="none"};svgedit.draw.Drawing.prototype.setLayerVisibility=function(s,v){if(typeof v!="boolean")return null;for(var G=null,e=0;e<this.getNumLayers();++e)if(this.getLayerName(e)==s){G=this.all_layers[e][1]; -break}if(!G)return null;G.getAttribute("display");G.setAttribute("display",v?"inline":"none");return G};svgedit.draw.Drawing.prototype.getLayerOpacity=function(s){for(var v=0;v<this.getNumLayers();++v)if(this.getLayerName(v)==s){(s=this.all_layers[v][1].getAttribute("opacity"))||(s="1.0");return parseFloat(s)}return null};svgedit.draw.Drawing.prototype.setLayerOpacity=function(s,v){if(!(typeof v!="number"||v<0||v>1))for(var G=0;G<this.getNumLayers();++G)if(this.getLayerName(G)==s){this.all_layers[G][1].setAttribute("opacity", -v);break}}})();svgedit=svgedit||{}; -(function(){if(!svgedit.path)svgedit.path={};var a={pathNodeTooltip:"Drag node to move it. Double-click node to change segment type",pathCtrlPtTooltip:"Drag control point to adjust curve properties"},J={2:["x","y"],4:["x","y"],6:["x","y","x1","y1","x2","y2"],8:["x","y","x1","y1"],10:["x","y","r1","r2","angle","largeArcFlag","sweepFlag"],12:["x"],14:["y"],16:["x","y","x2","y2"],18:["x","y"]},l=[],s=true,v={};svgedit.path.setLinkControlPoints=function(f){s=f};var G=svgedit.path.path=null;svgedit.path.init= -function(f){G=f;l=[0,"ClosePath"];$.each(["Moveto","Lineto","CurvetoCubic","CurvetoQuadratic","Arc","LinetoHorizontal","LinetoVertical","CurvetoCubicSmooth","CurvetoQuadraticSmooth"],function(k,n){l.push(n+"Abs");l.push(n+"Rel")})};svgedit.path.insertItemBefore=function(f,k,n){f=f.pathSegList;if(svgedit.browser.supportsPathInsertItemBefore())f.insertItemBefore(k,n);else{for(var F=f.numberOfItems,B=[],A=0;A<F;A++){var O=f.getItem(A);B.push(O)}f.clear();for(A=0;A<F;A++){A==n&&f.appendItem(k);f.appendItem(B[A])}}}; -svgedit.path.ptObjToArr=function(f,k){for(var n=J[f],F=n.length,B=Array(F),A=0;A<F;A++)B[A]=k[n[A]];return B};svgedit.path.getGripPt=function(f,k){var n={x:k?k.x:f.item.x,y:k?k.y:f.item.y},F=f.path;if(F.matrix)n=svgedit.math.transformPoint(n.x,n.y,F.matrix);n.x*=G.getCurrentZoom();n.y*=G.getCurrentZoom();return n};svgedit.path.getPointFromGrip=function(f,k){var n={x:f.x,y:f.y};if(k.matrix){f=svgedit.math.transformPoint(n.x,n.y,k.imatrix);n.x=f.x;n.y=f.y}n.x/=G.getCurrentZoom();n.y/=G.getCurrentZoom(); -return n};svgedit.path.addPointGrip=function(f,k,n){var F=svgedit.path.getGripContainer(),B=svgedit.utilities.getElem("pathpointgrip_"+f);if(!B){B=document.createElementNS("http://www.w3.org/2000/svg","circle");svgedit.utilities.assignAttributes(B,{id:"pathpointgrip_"+f,display:"none",r:4,fill:"#0FF",stroke:"#00F","stroke-width":2,cursor:"move",style:"pointer-events:all","xlink:title":a.pathNodeTooltip});B=F.appendChild(B);$("#pathpointgrip_"+f).dblclick(function(){svgedit.path.path&&svgedit.path.path.setSegType()})}k&& -n&&svgedit.utilities.assignAttributes(B,{cx:k,cy:n,display:"inline"});return B};svgedit.path.getGripContainer=function(){var f=svgedit.utilities.getElem("pathpointgrip_container");if(!f){f=svgedit.utilities.getElem("selectorParentGroup").appendChild(document.createElementNS("http://www.w3.org/2000/svg","g"));f.id="pathpointgrip_container"}return f};svgedit.path.addCtrlGrip=function(f){var k=svgedit.utilities.getElem("ctrlpointgrip_"+f);if(k)return k;k=document.createElementNS("http://www.w3.org/2000/svg", -"circle");svgedit.utilities.assignAttributes(k,{id:"ctrlpointgrip_"+f,display:"none",r:4,fill:"#0FF",stroke:"#55F","stroke-width":1,cursor:"move",style:"pointer-events:all","xlink:title":a.pathCtrlPtTooltip});svgedit.path.getGripContainer().appendChild(k);return k};svgedit.path.getCtrlLine=function(f){var k=svgedit.utilities.getElem("ctrlLine_"+f);if(k)return k;k=document.createElementNS("http://www.w3.org/2000/svg","line");svgedit.utilities.assignAttributes(k,{id:"ctrlLine_"+f,stroke:"#555","stroke-width":1, -style:"pointer-events:none"});svgedit.path.getGripContainer().appendChild(k);return k};svgedit.path.getPointGrip=function(f,k){var n=svgedit.path.addPointGrip(f.index);if(k){var F=svgedit.path.getGripPt(f);svgedit.utilities.assignAttributes(n,{cx:F.x,cy:F.y,display:"inline"})}return n};svgedit.path.getControlPoints=function(f){var k=f.item,n=f.index;if(!("x1"in k)||!("x2"in k))return null;var F={};svgedit.path.getGripContainer();for(var B=[svgedit.path.path.segs[n-1].item,k],A=1;A<3;A++){var O=n+ -"c"+A,Z=F["c"+A+"_line"]=svgedit.path.getCtrlLine(O),N=svgedit.path.getGripPt(f,{x:k["x"+A],y:k["y"+A]}),L=svgedit.path.getGripPt(f,{x:B[A-1].x,y:B[A-1].y});svgedit.utilities.assignAttributes(Z,{x1:N.x,y1:N.y,x2:L.x,y2:L.y,display:"inline"});F["c"+A+"_line"]=Z;pointGrip=F["c"+A]=svgedit.path.addCtrlGrip(O);svgedit.utilities.assignAttributes(pointGrip,{cx:N.x,cy:N.y,display:"inline"});F["c"+A]=pointGrip}return F};svgedit.path.replacePathSeg=function(f,k,n,F){F=F||svgedit.path.path.elem;f=F["createSVGPathSeg"+ -l[f]].apply(F,n);if(svgedit.browser.supportsPathReplaceItem())F.pathSegList.replaceItem(f,k);else{n=F.pathSegList;F=n.numberOfItems;for(var B=[],A=0;A<F;A++){var O=n.getItem(A);B.push(O)}n.clear();for(A=0;A<F;A++)A==k?n.appendItem(f):n.appendItem(B[A])}};svgedit.path.getSegSelector=function(f,k){var n=f.index,F=svgedit.utilities.getElem("segline_"+n);if(!F){var B=svgedit.path.getGripContainer();F=document.createElementNS("http://www.w3.org/2000/svg","path");svgedit.utilities.assignAttributes(F,{id:"segline_"+ -n,display:"none",fill:"none",stroke:"#0FF","stroke-width":2,style:"pointer-events:none",d:"M0,0 0,0"});B.appendChild(F)}if(k){n=f.prev;if(!n){F.setAttribute("display","none");return F}n=svgedit.path.getGripPt(n);svgedit.path.replacePathSeg(2,0,[n.x,n.y],F);B=svgedit.path.ptObjToArr(f.type,f.item,true);for(var A=0;A<B.length;A+=2){n=svgedit.path.getGripPt(f,{x:B[A],y:B[A+1]});B[A]=n.x;B[A+1]=n.y}svgedit.path.replacePathSeg(f.type,1,B,F)}return F};svgedit.path.smoothControlPoints=this.smoothControlPoints= -function(f,k,n){var F=f.x-n.x,B=f.y-n.y,A=k.x-n.x,O=k.y-n.y;if((F!=0||B!=0)&&(A!=0||O!=0)){f=Math.atan2(B,F);k=Math.atan2(O,A);F=Math.sqrt(F*F+B*B);A=Math.sqrt(A*A+O*O);B=G.getSVGRoot().createSVGPoint();O=G.getSVGRoot().createSVGPoint();if(f<0)f+=2*Math.PI;if(k<0)k+=2*Math.PI;var Z=Math.abs(f-k),N=Math.abs(Math.PI-Z)/2;if(f-k>0){f=Z<Math.PI?f+N:f-N;k=Z<Math.PI?k-N:k+N}else{f=Z<Math.PI?f-N:f+N;k=Z<Math.PI?k+N:k-N}B.x=F*Math.cos(f)+n.x;B.y=F*Math.sin(f)+n.y;O.x=A*Math.cos(k)+n.x;O.y=A*Math.sin(k)+n.y; -return[B,O]}};svgedit.path.Segment=function(f,k){this.selected=false;this.index=f;this.item=k;this.type=k.pathSegType;this.ctrlpts=[];this.segsel=this.ptgrip=null};svgedit.path.Segment.prototype.showCtrlPts=function(f){for(var k in this.ctrlpts)this.ctrlpts[k].setAttribute("display",f?"inline":"none")};svgedit.path.Segment.prototype.selectCtrls=function(f){$("#ctrlpointgrip_"+this.index+"c1, #ctrlpointgrip_"+this.index+"c2").attr("fill",f?"#0FF":"#EEE")};svgedit.path.Segment.prototype.show=function(f){if(this.ptgrip){this.ptgrip.setAttribute("display", -f?"inline":"none");this.segsel.setAttribute("display",f?"inline":"none");this.showCtrlPts(f)}};svgedit.path.Segment.prototype.select=function(f){if(this.ptgrip){this.ptgrip.setAttribute("stroke",f?"#0FF":"#00F");this.segsel.setAttribute("display",f?"inline":"none");this.ctrlpts&&this.selectCtrls(f);this.selected=f}};svgedit.path.Segment.prototype.addGrip=function(){this.ptgrip=svgedit.path.getPointGrip(this,true);this.ctrlpts=svgedit.path.getControlPoints(this,true);this.segsel=svgedit.path.getSegSelector(this, -true)};svgedit.path.Segment.prototype.update=function(f){if(this.ptgrip){var k=svgedit.path.getGripPt(this);svgedit.utilities.assignAttributes(this.ptgrip,{cx:k.x,cy:k.y});svgedit.path.getSegSelector(this,true);if(this.ctrlpts){if(f){this.item=svgedit.path.path.elem.pathSegList.getItem(this.index);this.type=this.item.pathSegType}svgedit.path.getControlPoints(this)}}};svgedit.path.Segment.prototype.move=function(f,k){var n=this.item;n=this.ctrlpts?[n.x+=f,n.y+=k,n.x1,n.y1,n.x2+=f,n.y2+=k]:[n.x+=f, -n.y+=k];svgedit.path.replacePathSeg(this.type,this.index,n);if(this.next&&this.next.ctrlpts){n=this.next.item;n=[n.x,n.y,n.x1+=f,n.y1+=k,n.x2,n.y2];svgedit.path.replacePathSeg(this.next.type,this.next.index,n)}if(this.mate){n=this.mate.item;n=[n.x+=f,n.y+=k];svgedit.path.replacePathSeg(this.mate.type,this.mate.index,n)}this.update(true);this.next&&this.next.update(true)};svgedit.path.Segment.prototype.setLinked=function(f){var k,n,F;if(f==2){n=1;k=this.next;if(!k)return;F=this.item}else{n=2;k=this.prev; -if(!k)return;F=k.item}var B=k.item;B["x"+n]=F.x+(F.x-this.item["x"+f]);B["y"+n]=F.y+(F.y-this.item["y"+f]);svgedit.path.replacePathSeg(k.type,k.index,[B.x,B.y,B.x1,B.y1,B.x2,B.y2]);k.update(true)};svgedit.path.Segment.prototype.moveCtrl=function(f,k,n){var F=this.item;F["x"+f]+=k;F["y"+f]+=n;svgedit.path.replacePathSeg(this.type,this.index,[F.x,F.y,F.x1,F.y1,F.x2,F.y2]);this.update(true)};svgedit.path.Segment.prototype.setType=function(f,k){svgedit.path.replacePathSeg(f,this.index,k);this.type=f; -this.item=svgedit.path.path.elem.pathSegList.getItem(this.index);this.showCtrlPts(f===6);this.ctrlpts=svgedit.path.getControlPoints(this);this.update(true)};svgedit.path.Path=function(f){if(!f||f.tagName!=="path")throw"svgedit.path.Path constructed without a <path> element";this.elem=f;this.segs=[];this.selected_pts=[];svgedit.path.path=this;this.init()};svgedit.path.Path.prototype.init=function(){$(svgedit.path.getGripContainer()).find("*").attr("display","none");var f=this.elem.pathSegList,k=f.numberOfItems; -this.segs=[];this.selected_pts=[];this.first_seg=null;for(var n=0;n<k;n++){var F=f.getItem(n);F=new svgedit.path.Segment(n,F);F.path=this;this.segs.push(F)}f=this.segs;F=null;for(n=0;n<k;n++){var B=f[n],A=n+1>=k?null:f[n+1],O=n-1<0?null:f[n-1];if(B.type===2){if(O&&O.type!==1){A=f[F];A.next=f[F+1];A.next.prev=A;A.addGrip()}F=n}else if(A&&A.type===1){B.next=f[F+1];B.next.prev=B;B.mate=f[F];B.addGrip();if(this.first_seg==null)this.first_seg=B}else if(A){if(B.type!==1){B.addGrip();if(A&&A.type!==2){B.next= -A;B.next.prev=B}}}else if(B.type!==1){A=f[F];A.next=f[F+1];A.next.prev=A;A.addGrip();B.addGrip();if(!this.first_seg)this.first_seg=f[F]}}return this};svgedit.path.Path.prototype.eachSeg=function(f){for(var k=this.segs.length,n=0;n<k;n++)if(f.call(this.segs[n],n)===false)break};svgedit.path.Path.prototype.addSeg=function(f){var k=this.segs[f];if(k.prev){var n=k.prev,F;switch(k.item.pathSegType){case 4:var B=(k.item.x+n.item.x)/2,A=(k.item.y+n.item.y)/2;F=this.elem.createSVGPathSegLinetoAbs(B,A);break; -case 6:F=(n.item.x+k.item.x1)/2;var O=(k.item.x1+k.item.x2)/2,Z=(k.item.x2+k.item.x)/2,N=(F+O)/2;O=(O+Z)/2;B=(N+O)/2;var L=(n.item.y+k.item.y1)/2,qa=(k.item.y1+k.item.y2)/2;n=(k.item.y2+k.item.y)/2;var aa=(L+qa)/2;qa=(qa+n)/2;A=(aa+qa)/2;F=this.elem.createSVGPathSegCurvetoCubicAbs(B,A,F,L,N,aa);svgedit.path.replacePathSeg(k.type,f,[k.item.x,k.item.y,O,qa,Z,n])}svgedit.path.insertItemBefore(this.elem,F,f)}};svgedit.path.Path.prototype.deleteSeg=function(f){var k=this.segs[f],n=this.elem.pathSegList; -k.show(false);var F=k.next;if(k.mate){var B=[F.item.x,F.item.y];svgedit.path.replacePathSeg(2,F.index,B);svgedit.path.replacePathSeg(4,k.index,B);n.removeItem(k.mate.index)}else{if(!k.prev){B=[F.item.x,F.item.y];svgedit.path.replacePathSeg(2,k.next.index,B)}n.removeItem(f)}};svgedit.path.Path.prototype.subpathIsClosed=function(f){var k=false;svgedit.path.path.eachSeg(function(n){if(n<=f)return true;if(this.type===2)return false;else if(this.type===1){k=true;return false}});return k};svgedit.path.Path.prototype.removePtFromSelection= -function(f){var k=this.selected_pts.indexOf(f);if(k!=-1){this.segs[f].select(false);this.selected_pts.splice(k,1)}};svgedit.path.Path.prototype.clearSelection=function(){this.eachSeg(function(){this.select(false)});this.selected_pts=[]};svgedit.path.Path.prototype.storeD=function(){this.last_d=this.elem.getAttribute("d")};svgedit.path.Path.prototype.show=function(f){this.eachSeg(function(){this.show(f)});f&&this.selectPt(this.first_seg.index);return this};svgedit.path.Path.prototype.movePts=function(f, -k){for(var n=this.selected_pts.length;n--;)this.segs[this.selected_pts[n]].move(f,k)};svgedit.path.Path.prototype.moveCtrl=function(f,k){var n=this.segs[this.selected_pts[0]];n.moveCtrl(this.dragctrl,f,k);s&&n.setLinked(this.dragctrl)};svgedit.path.Path.prototype.setSegType=function(f){this.storeD();for(var k=this.selected_pts.length,n;k--;){var F=this.segs[this.selected_pts[k]],B=F.prev;if(B){if(!f){n="Toggle Path Segment Type";f=F.type==6?4:6}f-=0;var A=F.item.x,O=F.item.y,Z=B.item.x;B=B.item.y; -var N;switch(f){case 6:if(F.olditem){Z=F.olditem;N=[A,O,Z.x1,Z.y1,Z.x2,Z.y2]}else{N=A-Z;var L=O-B;N=[A,O,Z+N/3,B+L/3,A-N/3,O-L/3]}break;case 4:N=[A,O];F.olditem=F.item}F.setType(f,N)}}svgedit.path.path.endChanges(n)};svgedit.path.Path.prototype.selectPt=function(f,k){this.clearSelection();f==null&&this.eachSeg(function(n){if(this.prev)f=n});this.addPtsToSelection(f);if(k){this.dragctrl=k;s&&this.segs[f].setLinked(k)}};svgedit.path.Path.prototype.update=function(){var f=this.elem;if(svgedit.utilities.getRotationAngle(f)){this.matrix= -svgedit.math.getMatrix(f);this.imatrix=this.matrix.inverse()}else this.imatrix=this.matrix=null;this.eachSeg(function(k){this.item=f.pathSegList.getItem(k);this.update()});return this};svgedit.path.getPath_=function(f){var k=v[f.id];k||(k=v[f.id]=new svgedit.path.Path(f));return k};svgedit.path.removePath_=function(f){f in v&&delete v[f]};var e=function(f,k){dx=f-oldcx;dy=k-oldcy;r=Math.sqrt(dx*dx+dy*dy);theta=Math.atan2(dy,dx)+angle;dx=r*Math.cos(theta)+oldcx;dy=r*Math.sin(theta)+oldcy;dx-=newcx; -dy-=newcy;r=Math.sqrt(dx*dx+dy*dy);theta=Math.atan2(dy,dx)-angle;return{x:(r*Math.cos(theta)+newcx)/1,y:(r*Math.sin(theta)+newcy)/1}};svgedit.path.recalcRotatedPath=function(){var f=svgedit.path.path.elem,k=svgedit.utilities.getRotationAngle(f,true);if(k){var n=svgedit.utilities.getBBox(f),F=svgedit.path.path.oldbbox,B=F.x+F.width/2,A=F.y+F.height/2;F=n.x+n.width/2;n=n.y+n.height/2;F=F-B;var O=n-A;n=Math.sqrt(F*F+O*O);O=Math.atan2(O,F)+k;F=n*Math.cos(O)+B;n=n*Math.sin(O)+A;B=f.pathSegList;for(A=B.numberOfItems;A;){A-= -1;O=B.getItem(A);var Z=O.pathSegType;if(Z!=1){var N=e(O.x,O.y);N=[N.x,N.y];if(O.x1!=null&&O.x2!=null){c_vals1=e(O.x1,O.y1);c_vals2=e(O.x2,O.y2);N.splice(N.length,0,c_vals1.x,c_vals1.y,c_vals2.x,c_vals2.y)}svgedit.path.replacePathSeg(Z,A,N)}}svgedit.utilities.getBBox(f);B=svgroot.createSVGTransform();f=svgedit.transformlist.getTransformList(f);B.setRotate(k*180/Math.PI,F,n);f.replaceItem(B,0)}};svgedit.path.clearData=function(){v={}}})();if(!window.console){window.console={};window.console.log=function(){};window.console.dir=function(){}}if(window.opera){window.console.log=function(a){opera.postError(a)};window.console.dir=function(){}} -(function(){var a=jQuery.fn.attr;jQuery.fn.attr=function(J,l){var s=this.length;if(!s)return a.apply(this,arguments);for(var v=0;v<s;v++){var G=this[v];if(G.namespaceURI==="http://www.w3.org/2000/svg")if(l!==undefined)G.setAttribute(J,l);else if($.isArray(J)){s=J.length;for(v={};s--;){var e=J[s],f=G.getAttribute(e);if(f||f==="0")f=isNaN(f)?f:f-0;v[e]=f}return v}else if(typeof J==="object")for(e in J)G.setAttribute(e,J[e]);else{if((f=G.getAttribute(J))||f==="0")f=isNaN(f)?f:f-0;return f}else return a.apply(this, -arguments)}return this}})(); -$.SvgCanvas=function(a,J){function l(b,c){for(var d=svgedit.utilities.getBBox(b),m=0;m<2;m++){var i=m===0?"fill":"stroke",z=b.getAttribute(i);if(z&&z.indexOf("url(")===0){z=rb(z);if(z.tagName==="linearGradient"){var o=z.getAttribute("x1")||0,h=z.getAttribute("y1")||0,q=z.getAttribute("x2")||1,w=z.getAttribute("y2")||0;o=d.width*o+d.x;h=d.height*h+d.y;q=d.width*q+d.x;w=d.height*w+d.y;o=ja(o,h,c);w=ja(q,w,c);q={};q.x1=(o.x-d.x)/d.width;q.y1=(o.y-d.y)/d.height;q.x2=(w.x-d.x)/d.width;q.y2=(w.y-d.y)/d.height; -z=z.cloneNode(true);$(z).attr(q);z.id=Ga();Ta().appendChild(z);b.setAttribute(i,"url(#"+z.id+")")}}}}var s="http://www.w3.org/2000/svg",v={show_outside_canvas:true,selectNew:true,dimensions:[640,480]};J&&$.extend(v,J);var G=v.dimensions,e=this,f=a.ownerDocument,k=f.importNode(svgedit.utilities.text2xml('<svg id="svgroot" xmlns="'+s+'" xlinkns="http://www.w3.org/1999/xlink" width="'+G[0]+'" height="'+G[1]+'" x="'+G[0]+'" y="'+G[1]+'" overflow="visible"><defs><filter id="canvashadow" filterUnits="objectBoundingBox"><feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/><feOffset in="blur" dx="5" dy="5" result="offsetBlur"/><feMerge><feMergeNode in="offsetBlur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs></svg>').documentElement, -true);a.appendChild(k);var n=f.createElementNS(s,"svg");(e.clearSvgContentElement=function(){for(;n.firstChild;)n.removeChild(n.firstChild);$(n).attr({id:"svgcontent",width:G[0],height:G[1],x:G[0],y:G[1],overflow:v.show_outside_canvas?"visible":"hidden",xmlns:s,"xmlns:se":"http://svg-edit.googlecode.com","xmlns:xlink":"http://www.w3.org/1999/xlink"}).appendTo(k);var b=f.createComment(" Created with SVG-edit - http://svg-edit.googlecode.com/ ");n.appendChild(b)})();var F="svg_";e.setIdPrefix=function(b){F= -b};e.current_drawing_=new svgedit.draw.Drawing(n,F);var B=e.getCurrentDrawing=function(){return e.current_drawing_},A=1,O=null,Z={shape:{fill:(v.initFill.color=="none"?"":"#")+v.initFill.color,fill_paint:null,fill_opacity:v.initFill.opacity,stroke:"#"+v.initStroke.color,stroke_paint:null,stroke_opacity:v.initStroke.opacity,stroke_width:v.initStroke.width,stroke_dasharray:"none",stroke_linejoin:"miter",stroke_linecap:"butt",opacity:v.initOpacity}};Z.text=$.extend(true,{},Z.shape);$.extend(Z.text,{fill:"#000000", -stroke_width:v.text.stroke_width,font_size:v.text.font_size,font_family:v.text.font_family});var N=Z.shape,L=Array(1),qa=this.addSvgElementFromJson=function(b){var c=svgedit.utilities.getElem(b.attr.id),d=B().getCurrentLayer();if(c&&b.element!=c.tagName){d.removeChild(c);c=null}if(!c){c=f.createElementNS(s,b.element);if(d)(O||d).appendChild(c)}b.curStyles&&svgedit.utilities.assignAttributes(c,{fill:N.fill,stroke:N.stroke,"stroke-width":N.stroke_width,"stroke-dasharray":N.stroke_dasharray,"stroke-linejoin":N.stroke_linejoin, -"stroke-linecap":N.stroke_linecap,"stroke-opacity":N.stroke_opacity,"fill-opacity":N.fill_opacity,opacity:N.opacity,style:"pointer-events:inherit"},100);svgedit.utilities.assignAttributes(c,b.attr,100);svgedit.utilities.cleanupElement(c);return c},aa=e.getTransformList=svgedit.transformlist.getTransformList,ja=svgedit.math.transformPoint,da=e.matrixMultiply=svgedit.math.matrixMultiply,na=e.hasMatrixTransform=svgedit.math.hasMatrixTransform,ka=e.transformListToTransform=svgedit.math.transformListToTransform, -Ra=svgedit.math.snapToAngle,$b=svgedit.math.getMatrix;svgedit.units.init({getBaseUnit:function(){return v.baseUnit},getElement:svgedit.utilities.getElem,getHeight:function(){return n.getAttribute("height")/A},getWidth:function(){return n.getAttribute("width")/A},getRoundDigits:function(){return lb.round_digits}});var jb=e.convertToNum=svgedit.units.convertToNum;svgedit.utilities.init({getDOMDocument:function(){return f},getDOMContainer:function(){return a},getSVGRoot:function(){return k},getSelectedElements:function(){return L}, -getSVGContent:function(){return n}});var ba=e.getUrlFromAttr=svgedit.utilities.getUrlFromAttr,gb=e.getHref=svgedit.utilities.getHref,hb=e.setHref=svgedit.utilities.setHref,ac=svgedit.utilities.getPathBBox;e.getBBox=svgedit.utilities.getBBox;var Va=e.getRotationAngle=svgedit.utilities.getRotationAngle,ya=e.getElem=svgedit.utilities.getElem,Da=e.assignAttributes=svgedit.utilities.assignAttributes,Ob=this.cleanupElement=svgedit.utilities.cleanupElement,Ka=svgedit.sanitize.getNSMap(),sb=e.sanitizeSvg= -svgedit.sanitize.sanitizeSvg,ha=svgedit.history.MoveElementCommand,xa=svgedit.history.InsertElementCommand,La=svgedit.history.RemoveElementCommand,Ha=svgedit.history.ChangeElementCommand,ta=svgedit.history.BatchCommand;e.undoMgr=new svgedit.history.UndoManager({handleHistoryEvent:function(b,c){var d=svgedit.history.HistoryEventTypes;if(b==d.BEFORE_UNAPPLY||b==d.BEFORE_APPLY)e.clearSelection();else if(b==d.AFTER_APPLY||b==d.AFTER_UNAPPLY){var m=c.elements();e.pathActions.clear();ia("changed",m);m= -c.type();d=b==d.AFTER_APPLY;if(m==ha.type())(d?c.newParent:c.oldParent)==n&&e.identifyLayers();else if(m==xa.type()||m==La.type()){c.parent==n&&e.identifyLayers();if(m==xa.type())d&&Db(c.elem);else d||Db(c.elem);c.elem.tagName==="use"&&Eb(c.elem)}else if(m==Ha.type()){c.elem.tagName=="title"&&c.elem.parentNode.parentNode==n&&e.identifyLayers();d=d?c.newValues:c.oldValues;d.stdDeviation&&e.setBlurOffsets(c.elem.parentNode,d.stdDeviation)}}}});var oa=function(b){e.undoMgr.addCommandToHistory(b)};svgedit.select.init(v, -{createSVGElement:function(b){return e.addSvgElementFromJson(b)},svgRoot:function(){return k},svgContent:function(){return n},currentZoom:function(){return A},getStrokedBBox:function(b){return e.getStrokedBBox([b])}});var wa=this.selectorManager=svgedit.select.getSelectorManager();svgedit.path.init({getCurrentZoom:function(){return A},getSVGRoot:function(){return k}});svgedit.utilities.snapToGrid=function(b){var c=v.snappingStep,d=v.baseUnit;if(d!=="px")c*=svgedit.units.getTypeMap()[d];return b=Math.round(b/ -c)*c};var pa=svgedit.utilities.snapToGrid,mb={exportNoBlur:"Blurred elements will appear as un-blurred",exportNoforeignObject:"foreignObject elements will not appear",exportNoDashArray:"Strokes will appear filled",exportNoText:"Text may not appear as expected"},Pb=["clip-path","fill","filter","marker-end","marker-mid","marker-start","mask","stroke"],tb=$.data,Fb=document.createElementNS(s,"animate");$(Fb).attr({attributeName:"opacity",begin:"indefinite",dur:1,fill:"freeze"}).appendTo(k);var Db=function(b){var c= -$(b).attr(Pb),d;for(d in c){var m=c[d];if(m&&m.indexOf("url(")===0){m=ba(m).substr(1);if(!ya(m)){Ta().appendChild(Qb[m]);delete Qb[m]}}}b=b.getElementsByTagName("*");if(b.length){c=0;for(d=b.length;c<d;c++)Db(b[c])}},nb={},Rb=v.imgPath+"logo.png",ub=[],lb={round_digits:5},ra=false,Ma=null,va="select",Za="none",ob={},Ua=Z.text,Oa=N,Gb=null,la=null,fb=[],zb={},Hb=null,Qb={};e.clipBoard=[];var Ab=this.runExtensions=function(b,c,d){var m=false;if(d)m=[];$.each(zb,function(i,z){if(b in z)if(d)m.push(z[b](c)); -else m=z[b](c)});return m};this.addExtension=function(b,c){if(b in zb)console.log('Cannot add extension "'+b+'", an extension by that name already exists"');else{var d=$.isFunction(c)?c($.extend(e.getPrivateMethods(),{svgroot:k,svgcontent:n,nonce:B().getNonce(),selectorManager:wa})):c;zb[b]=d;ia("extension_added",d)}};var Bb=this.round=function(b){return parseInt(b*A)/A},Sb=this.getIntersectionList=function(b){if(la==null)return null;var c=O||B().getCurrentLayer();fb.length||(fb=kc(c));var d=null; -try{d=c.getIntersectionList(b,null)}catch(m){}if(d==null||typeof d.item!="function"){d=[];if(b)b=b;else{b=la.getBBox();c={};for(var i in b)c[i]=b[i]/A;b=c}for(i=fb.length;i--;)b.width&&b.width&&svgedit.math.rectsIntersect(b,fb[i].bbox)&&d.push(fb[i].elem)}return d};getStrokedBBox=this.getStrokedBBox=function(b){b||(b=Ib());if(!b.length)return false;var c=function(w){try{var u=svgedit.utilities.getBBox(w),t=svgedit.utilities.getRotationAngle(w);if(t&&t%90||svgedit.math.hasMatrixTransform(svgedit.transformlist.getTransformList(w))){t= -false;if(["ellipse","path","line","polyline","polygon"].indexOf(w.tagName)>=0)u=t=e.convertToPath(w,true);else if(w.tagName=="rect"){var C=w.getAttribute("rx"),E=w.getAttribute("ry");if(C||E)u=t=e.convertToPath(w,true)}if(!t){var H=w.cloneNode(true),I=document.createElementNS(s,"g"),P=w.parentNode;P.appendChild(I);I.appendChild(H);u=svgedit.utilities.bboxToObj(I.getBBox());P.removeChild(I)}}return u}catch(W){console.log(w,W);return null}},d;$.each(b,function(){if(!d)if(this.parentNode)d=c(this)}); -if(d==null)return null;var m=d.x+d.width,i=d.y+d.height,z=d.x,o=d.y,h=function(w){var u=w.getAttribute("stroke-width"),t=0;if(w.getAttribute("stroke")!="none"&&!isNaN(u))t+=u/2;return t},q=[];$.each(b,function(w,u){var t=c(u);if(t){var C=h(u);z=Math.min(z,t.x-C);o=Math.min(o,t.y-C);q.push(t)}});d.x=z;d.y=o;$.each(b,function(w,u){var t=q[w];if(t&&u.nodeType==1){var C=h(u);m=Math.max(m,t.x+t.width+C);i=Math.max(i,t.y+t.height+C)}});d.width=m-z;d.height=i-o;return d};var Ib=this.getVisibleElements=function(b){b|| -(b=$(n).children());var c=[];$(b).children().each(function(d,m){try{m.getBBox()&&c.push(m)}catch(i){}});return c.reverse()},kc=this.getVisibleElementsAndBBoxes=function(b){b||(b=$(n).children());var c=[];$(b).children().each(function(d,m){try{m.getBBox()&&c.push({elem:m,bbox:getStrokedBBox([m])})}catch(i){}});return c.reverse()},bc=this.groupSvgElem=function(b){var c=document.createElementNS(s,"g");b.parentNode.replaceChild(c,b);$(c).append(b).data("gsvg",b)[0].id=Ga()},vb=function(b){var c=document.createElementNS(b.namespaceURI, -b.nodeName);$.each(b.attributes,function(m,i){i.localName!="-moz-math-font-style"&&c.setAttributeNS(i.namespaceURI,i.nodeName,i.nodeValue)});c.removeAttribute("id");c.id=Ga();if(svgedit.browser.isWebkit()&&b.nodeName=="path"){var d=sa.convertPath(b);c.setAttribute("d",d)}$.each(b.childNodes,function(m,i){switch(i.nodeType){case 1:c.appendChild(vb(i));break;case 3:c.textContent=i.nodeValue}});if($(b).data("gsvg"))$(c).data("gsvg",c.firstChild);else if($(b).data("symbol")){b=$(b).data("symbol");$(c).data("ref", -b).data("symbol",b)}else c.tagName=="image"&&Jb(c);return c},$a,Ga,ia;(function(b){var c={};$a=b.getId=function(){return B().getId()};Ga=b.getNextId=function(){return B().getNextId()};ia=b.call=function(d,m){if(c[d])return c[d](this,m)};b.bind=function(d,m){var i=c[d];c[d]=m;return i}})(e);this.prepareSvg=function(b){this.sanitizeSvg(b.documentElement);b=b.getElementsByTagNameNS(s,"path");for(var c=0,d=b.length;c<d;++c){var m=b[c];m.setAttribute("d",sa.convertPath(m));sa.fixEnd(m)}};var rb=this.getRefElem= -function(b){return ya(ba(b).substr(1))},Tb=function(b){if(!svgedit.browser.isGecko())return b;var c=b.cloneNode(true);b.parentNode.insertBefore(c,b);b.parentNode.removeChild(b);wa.releaseSelector(b);L[0]=c;wa.requestSelector(c).showGrips(true);return c};this.setRotationAngle=function(b,c){b=parseFloat(b);var d=L[0],m=d.getAttribute("transform"),i=svgedit.utilities.getBBox(d),z=i.x+i.width/2,o=i.y+i.height/2;i=aa(d);i.numberOfItems>0&&i.getItem(0).type==4&&i.removeItem(0);if(b!=0){z=ja(z,o,ka(i).matrix); -o=k.createSVGTransform();o.setRotate(b,z.x,z.y);i.numberOfItems?i.insertItemBefore(o,0):i.appendItem(o)}else i.numberOfItems==0&&d.removeAttribute("transform");if(!c){i=d.getAttribute("transform");d.setAttribute("transform",m);Pa("transform",i,L);ia("changed",L)}ya("pathpointgrip_container");d=wa.requestSelector(L[0]);d.resize();d.updateGripCursors(b)};var Kb=this.recalculateAllSelectedDimensions=function(){for(var b=new ta(Za=="none"?"position":"size"),c=L.length;c--;){var d=Sa(L[c]);d&&b.addSubCommand(d)}if(!b.isEmpty()){oa(b); -ia("changed",L)}},Ub=[0,"z","M","m","L","l","C","c","Q","q","A","a","H","h","V","v","S","s","T","t"],cc=function(b){console.log([b.a,b.b,b.c,b.d,b.e,b.f])},Lb=this.remapElement=function(b,c,d){var m=v.gridSnapping&&b.parentNode.parentNode.localName==="svg",i=function(){if(m)for(var t in c)c[t]=pa(c[t]);Da(b,c,1E3,true)};box=svgedit.utilities.getBBox(b);for(var z=0;z<2;z++){var o=z===0?"fill":"stroke",h=b.getAttribute(o);if(h&&h.indexOf("url(")===0)if(d.a<0||d.d<0){h=rb(h).cloneNode(true);if(d.a<0){var q= -h.getAttribute("x1"),w=h.getAttribute("x2");h.setAttribute("x1",-(q-1));h.setAttribute("x2",-(w-1))}if(d.d<0){q=h.getAttribute("y1");w=h.getAttribute("y2");h.setAttribute("y1",-(q-1));h.setAttribute("y2",-(w-1))}h.id=Ga();Ta().appendChild(h);b.setAttribute(o,"url(#"+h.id+")")}}z=b.tagName;if(z==="g"||z==="text"||z==="use")if(d.a==1&&d.b==0&&d.c==0&&d.d==1&&(d.e!=0||d.f!=0)){o=ka(b).matrix;o=da(o.inverse(),d,o);c.x=parseFloat(c.x)+o.e;c.y=parseFloat(c.y)+o.f}else{o=aa(b);h=k.createSVGTransform();h.setMatrix(da(ka(o).matrix, -d));o.clear();o.appendItem(h)}switch(z){case "foreignObject":case "rect":case "image":if(z==="image"&&(d.a<0||d.d<0)){o=aa(b);h=k.createSVGTransform();h.setMatrix(da(ka(o).matrix,d));o.clear();o.appendItem(h)}else{o=ja(c.x,c.y,d);c.width=d.a*c.width;c.height=d.d*c.height;c.x=o.x+Math.min(0,c.width);c.y=o.y+Math.min(0,c.height);c.width=Math.abs(c.width);c.height=Math.abs(c.height)}i();break;case "ellipse":z=ja(c.cx,c.cy,d);c.cx=z.x;c.cy=z.y;c.rx=d.a*c.rx;c.ry=d.d*c.ry;c.rx=Math.abs(c.rx);c.ry=Math.abs(c.ry); -i();break;case "circle":z=ja(c.cx,c.cy,d);c.cx=z.x;c.cy=z.y;d=svgedit.math.transformBox(box.x,box.y,box.width,box.height,d);c.r=Math.min((d.tr.x-d.tl.x)/2,(d.bl.y-d.tl.y)/2);if(c.r)c.r=Math.abs(c.r);i();break;case "line":o=ja(c.x1,c.y1,d);q=ja(c.x2,c.y2,d);c.x1=o.x;c.y1=o.y;c.x2=q.x;c.y2=q.y;case "text":case "use":i();break;case "g":(d=$(b).data("gsvg"))&&Da(d,c,1E3,true);break;case "polyline":case "polygon":i=c.points.length;for(z=0;z<i;++z){w=c.points[z];w=ja(w.x,w.y,d);c.points[z].x=w.x;c.points[z].y= -w.y}i=c.points.length;d="";for(z=0;z<i;++z){w=c.points[z];d+=w.x+","+w.y+" "}b.setAttribute("points",d);break;case "path":o=b.pathSegList;i=o.numberOfItems;c.d=Array(i);for(z=0;z<i;++z){h=o.getItem(z);c.d[z]={type:h.pathSegType,x:h.x,y:h.y,x1:h.x1,y1:h.y1,x2:h.x2,y2:h.y2,r1:h.r1,r2:h.r2,angle:h.angle,largeArcFlag:h.largeArcFlag,sweepFlag:h.sweepFlag}}i=c.d.length;z=c.d[0];var u=ja(z.x,z.y,d);c.d[0].x=u.x;c.d[0].y=u.y;for(z=1;z<i;++z){h=c.d[z];o=h.type;if(o%2==0){w=ja(h.x!=undefined?h.x:u.x,h.y!=undefined? -h.y:u.y,d);o=ja(h.x1,h.y1,d);q=ja(h.x2,h.y2,d);h.x=w.x;h.y=w.y;h.x1=o.x;h.y1=o.y;h.x2=q.x;h.y2=q.y}else{h.x=d.a*h.x;h.y=d.d*h.y;h.x1=d.a*h.x1;h.y1=d.d*h.y1;h.x2=d.a*h.x2;h.y2=d.d*h.y2}h.r1=d.a*h.r1;h.r2=d.d*h.r2}d="";i=c.d.length;for(z=0;z<i;++z){h=c.d[z];o=h.type;d+=Ub[o];switch(o){case 13:case 12:d+=h.x+" ";break;case 15:case 14:d+=h.y+" ";break;case 3:case 5:case 19:case 2:case 4:case 18:d+=h.x+","+h.y+" ";break;case 7:case 6:d+=h.x1+","+h.y1+" "+h.x2+","+h.y2+" "+h.x+","+h.y+" ";break;case 9:case 8:d+= -h.x1+","+h.y1+" "+h.x+","+h.y+" ";break;case 11:case 10:d+=h.r1+","+h.r2+" "+h.angle+" "+ +h.largeArcFlag+" "+ +h.sweepFlag+" "+h.x+","+h.y+" ";break;case 17:case 16:d+=h.x2+","+h.y2+" "+h.x+","+h.y+" "}}b.setAttribute("d",d)}},dc=function(b,c,d){b=rb(b).firstChild;var m=aa(b),i=k.createSVGTransform();i.setTranslate(c,d);m.appendItem(i);Sa(b)},Sa=this.recalculateDimensions=function(b){if(b==null)return null;var c=aa(b);if(c&&c.numberOfItems>0){for(var d=c.numberOfItems;d--;){var m=c.getItem(d);if(m.type=== -0)c.removeItem(d);else if(m.type===1)svgedit.math.isIdentity(m.matrix)&&c.removeItem(d);else m.type===4&&m.angle===0&&c.removeItem(d)}if(c.numberOfItems===1&&Va(b))return null}if(!c||c.numberOfItems==0){b.removeAttribute("transform");return null}if(c){d=c.numberOfItems;for(var i=[];d--;){m=c.getItem(d);if(m.type===1)i.push([m.matrix,d]);else if(i.length)i=[]}if(i.length===2){d=k.createSVGTransformFromMatrix(da(i[1][0],i[0][0]));c.removeItem(i[0][1]);c.removeItem(i[1][1]);c.insertItemBefore(d,i[1][1])}d= -c.numberOfItems;if(d>=2&&c.getItem(d-2).type===1&&c.getItem(d-1).type===2){i=k.createSVGTransform();m=da(c.getItem(d-2).matrix,c.getItem(d-1).matrix);i.setMatrix(m);c.removeItem(d-2);c.removeItem(d-2);c.appendItem(i)}}switch(b.tagName){case "line":case "polyline":case "polygon":case "path":break;default:if(c.numberOfItems===1&&c.getItem(0).type===1||c.numberOfItems===2&&c.getItem(0).type===1&&c.getItem(0).type===4)return null}var z=$(b).data("gsvg");d=new ta("Transform");var o={},h=null;m=[];switch(b.tagName){case "line":m= -["x1","y1","x2","y2"];break;case "circle":m=["cx","cy","r"];break;case "ellipse":m=["cx","cy","rx","ry"];break;case "foreignObject":case "rect":case "image":m=["width","height","x","y"];break;case "use":case "text":m=["x","y"];break;case "polygon":case "polyline":h={};h.points=b.getAttribute("points");i=b.points;var q=i.numberOfItems;o.points=Array(q);for(var w=0;w<q;++w){var u=i.getItem(w);o.points[w]={x:u.x,y:u.y}}break;case "path":h={};h.d=b.getAttribute("d");o.d=b.getAttribute("d")}if(m.length){o= -$(b).attr(m);$.each(o,function(ab,bb){o[ab]=jb(ab,bb)})}else if(z)o={x:$(z).attr("x")||0,y:$(z).attr("y")||0};if(h==null){h=$.extend(true,{},o);$.each(h,function(ab,bb){h[ab]=jb(ab,bb)})}h.transform=Ma?Ma:"";if(b.tagName=="g"&&!z||b.tagName=="a"){i=svgedit.utilities.getBBox(b);var t={x:i.x+i.width/2,y:i.y+i.height/2},C=ja(i.x+i.width/2,i.y+i.height/2,ka(c).matrix);m=k.createSVGMatrix();if(i=Va(b)){w=i*Math.PI/180;q=Math.abs(w)>1.0E-10?Math.sin(w)/(1-Math.cos(w)):2/w;for(w=0;w<c.numberOfItems;++w){m= -c.getItem(w);if(m.type==4){m=m.matrix;t.y=(q*m.e+m.f)/2;t.x=(m.e-q*m.f)/2;c.removeItem(w);break}}}w=m=z=0;var E=c.numberOfItems;if(E)var H=c.getItem(0).matrix;if(E>=3&&c.getItem(E-2).type==3&&c.getItem(E-3).type==2&&c.getItem(E-1).type==2){w=3;var I=c.getItem(E-3).matrix,P=c.getItem(E-2).matrix,W=c.getItem(E-1).matrix;q=b.childNodes;for(u=q.length;u--;){var Y=q.item(u);m=z=0;if(Y.nodeType==1){var R=aa(Y);if(R){m=ka(R).matrix;z=Va(Y);var T=Ma,V=[];Ma=Y.getAttribute("transform");if(z||na(R)){var ca= -k.createSVGTransform();ca.setMatrix(da(I,P,W,m));R.clear();R.appendItem(ca);V.push(ca)}else{z=da(m.inverse(),W,m);ca=k.createSVGMatrix();ca.e=-z.e;ca.f=-z.f;m=da(ca.inverse(),m.inverse(),I,P,W,m,z.inverse());var Ia=k.createSVGTransform(),Wa=k.createSVGTransform(),kb=k.createSVGTransform();Ia.setTranslate(z.e,z.f);Wa.setScale(m.a,m.d);kb.setTranslate(ca.e,ca.f);R.appendItem(kb);R.appendItem(Wa);R.appendItem(Ia);V.push(kb);V.push(Wa);V.push(Ia)}d.addSubCommand(Sa(Y));Ma=T}}}c.removeItem(E-1);c.removeItem(E- -2);c.removeItem(E-3)}else if(E>=3&&c.getItem(E-1).type==1){w=3;m=ka(c).matrix;ca=k.createSVGTransform();ca.setMatrix(m);c.clear();c.appendItem(ca)}else if((E==1||E>1&&c.getItem(1).type!=3)&&c.getItem(0).type==2){w=2;z=ka(c).matrix;c.removeItem(0);m=ka(c).matrix.inverse();m=da(m,z);z=m.e;m=m.f;if(z!=0||m!=0){q=b.childNodes;u=q.length;for(E=[];u--;){Y=q.item(u);if(Y.nodeType==1){if(Y.getAttribute("clip-path")){T=Y.getAttribute("clip-path");if(E.indexOf(T)===-1){dc(T,z,m);E.push(T)}}T=Ma;Ma=Y.getAttribute("transform"); -if(R=aa(Y)){I=k.createSVGTransform();I.setTranslate(z,m);R.numberOfItems?R.insertItemBefore(I,0):R.appendItem(I);d.addSubCommand(Sa(Y));R=b.getElementsByTagNameNS(s,"use");Y="#"+Y.id;for(I=R.length;I--;){P=R.item(I);if(Y==gb(P)){W=k.createSVGTransform();W.setTranslate(-z,-m);aa(P).insertItemBefore(W,0);d.addSubCommand(Sa(P))}}Ma=T}}}E=[];Ma=T}}else if(E==1&&c.getItem(0).type==1&&!i){w=1;m=c.getItem(0).matrix;q=b.childNodes;for(u=q.length;u--;){Y=q.item(u);if(Y.nodeType==1){T=Ma;Ma=Y.getAttribute("transform"); -if(R=aa(Y)){z=da(m,ka(R).matrix);E=k.createSVGTransform();E.setMatrix(z);R.clear();R.appendItem(E,0);d.addSubCommand(Sa(Y));Ma=T;T=Y.getAttribute("stroke-width");Y.getAttribute("stroke")!=="none"&&!isNaN(T)&&Y.setAttribute("stroke-width",T*((Math.abs(z.a)+Math.abs(z.d))/2))}}}c.clear()}else{if(i){t=k.createSVGTransform();t.setRotate(i,C.x,C.y);c.numberOfItems?c.insertItemBefore(t,0):c.appendItem(t)}c.numberOfItems==0&&b.removeAttribute("transform");return null}if(w==2){if(i){C={x:t.x+H.e,y:t.y+H.f}; -t=k.createSVGTransform();t.setRotate(i,C.x,C.y);c.numberOfItems?c.insertItemBefore(t,0):c.appendItem(t)}}else if(w==3){m=ka(c).matrix;H=k.createSVGTransform();H.setRotate(i,t.x,t.y);H=H.matrix;t=k.createSVGTransform();t.setRotate(i,C.x,C.y);C=t.matrix.inverse();T=m.inverse();C=da(T,C,H,m);z=C.e;m=C.f;if(z!=0||m!=0){q=b.childNodes;for(u=q.length;u--;){Y=q.item(u);if(Y.nodeType==1){T=Ma;Ma=Y.getAttribute("transform");R=aa(Y);I=k.createSVGTransform();I.setTranslate(z,m);R.numberOfItems?R.insertItemBefore(I, -0):R.appendItem(I);d.addSubCommand(Sa(Y));Ma=T}}}if(i)c.numberOfItems?c.insertItemBefore(t,0):c.appendItem(t)}}else{i=svgedit.utilities.getBBox(b);if(!i&&b.tagName!="path")return null;m=k.createSVGMatrix();if(z=Va(b)){t={x:i.x+i.width/2,y:i.y+i.height/2};C=ja(i.x+i.width/2,i.y+i.height/2,ka(c).matrix);w=z*Math.PI/180;q=Math.abs(w)>1.0E-10?Math.sin(w)/(1-Math.cos(w)):2/w;for(w=0;w<c.numberOfItems;++w){m=c.getItem(w);if(m.type==4){m=m.matrix;t.y=(q*m.e+m.f)/2;t.x=(m.e-q*m.f)/2;c.removeItem(w);break}}}w= -0;E=c.numberOfItems;if(!svgedit.browser.isWebkit())if((H=b.getAttribute("fill"))&&H.indexOf("url(")===0){H=rb(H);T="pattern";if(H.tagName!==T)T="gradient";if(H.getAttribute(T+"Units")==="userSpaceOnUse"){m=ka(c).matrix;i=aa(H);i=ka(i).matrix;m=da(m,i);i="matrix("+[m.a,m.b,m.c,m.d,m.e,m.f].join(",")+")";H.setAttribute(T+"Transform",i)}}if(E>=3&&c.getItem(E-2).type==3&&c.getItem(E-3).type==2&&c.getItem(E-1).type==2){w=3;m=ka(c,E-3,E-1).matrix;c.removeItem(E-1);c.removeItem(E-2);c.removeItem(E-3)}else if(E== -4&&c.getItem(E-1).type==1){w=3;m=ka(c).matrix;ca=k.createSVGTransform();ca.setMatrix(m);c.clear();c.appendItem(ca);m=k.createSVGMatrix()}else if((E==1||E>1&&c.getItem(1).type!=3)&&c.getItem(0).type==2){w=2;H=c.getItem(0).matrix;T=ka(c,1).matrix;i=T.inverse();m=da(i,H,T);c.removeItem(0)}else if(E==1&&c.getItem(0).type==1&&!z){m=ka(c).matrix;switch(b.tagName){case "line":o=$(b).attr(["x1","y1","x2","y2"]);case "polyline":case "polygon":o.points=b.getAttribute("points");if(o.points){i=b.points;q=i.numberOfItems; -o.points=Array(q);for(w=0;w<q;++w){u=i.getItem(w);o.points[w]={x:u.x,y:u.y}}}case "path":o.d=b.getAttribute("d");w=1;c.clear()}}else{w=4;if(z){t=k.createSVGTransform();t.setRotate(z,C.x,C.y);c.numberOfItems?c.insertItemBefore(t,0):c.appendItem(t)}c.numberOfItems==0&&b.removeAttribute("transform");return null}if(w==1||w==2||w==3)Lb(b,o,m);if(w==2){if(z){na(c)||(C={x:t.x+m.e,y:t.y+m.f});t=k.createSVGTransform();t.setRotate(z,C.x,C.y);c.numberOfItems?c.insertItemBefore(t,0):c.appendItem(t)}}else if(w== -3&&z){m=ka(c).matrix;H=k.createSVGTransform();H.setRotate(z,t.x,t.y);H=H.matrix;t=k.createSVGTransform();t.setRotate(z,C.x,C.y);C=t.matrix.inverse();T=m.inverse();C=da(T,C,H,m);Lb(b,o,C);if(z)c.numberOfItems?c.insertItemBefore(t,0):c.appendItem(t)}}c.numberOfItems==0&&b.removeAttribute("transform");d.addSubCommand(new Ha(b,h));return d},cb=null,Aa=this.clearSelection=function(b){if(L[0]!=null)for(var c=L.length,d=0;d<c;++d){var m=L[d];if(m==null)break;wa.releaseSelector(m);L[d]=null}b||ia("selected", -L)},db=this.addToSelection=function(b,c){if(b.length!=0){for(var d=0;d<L.length;){if(L[d]==null)break;++d}for(var m=b.length;m--;){var i=b[m];if(i&&svgedit.utilities.getBBox(i)){if(i.tagName==="a"&&i.childNodes.length===1)i=i.firstChild;if(L.indexOf(i)==-1){L[d]=i;d++;i=wa.requestSelector(i);L.length>1&&i.showGrips(false)}}}ia("selected",L);c||L.length==1?wa.requestSelector(L[0]).showGrips(true):wa.requestSelector(L[0]).showGrips(false);for(L.sort(function(z,o){if(z&&o&&z.compareDocumentPosition)return 3- -(o.compareDocumentPosition(z)&6);else if(z==null)return 1});L[0]==null;)L.shift(0)}},ib=this.selectOnly=function(b,c){Aa(true);db(b,c)};this.removeFromSelection=function(b){if(L[0]!=null)if(b.length!=0){var c=Array(L.length);j=0;len=L.length;for(var d=0;d<len;++d){var m=L[d];if(m)if(b.indexOf(m)==-1){c[j]=m;j++}else wa.releaseSelector(m)}L=c}};this.selectAllInCurrentLayer=function(){var b=B().getCurrentLayer();if(b){va="select";ib($(O||b).children())}};var eb=this.getMouseTarget=function(b){if(b== -null)return null;b=b.target;if(b.correspondingUseElement)b=b.correspondingUseElement;if(["http://www.w3.org/1998/Math/MathML","http://www.w3.org/1999/xhtml"].indexOf(b.namespaceURI)>=0&&b.id!="svgcanvas")for(;b.nodeName!="foreignObject";){b=b.parentNode;if(!b)return k}var c=B().getCurrentLayer();if([k,a,n,c].indexOf(b)>=0)return k;if($(b).closest("#selectorParentGroup").length)return wa.selectorParentGroup;for(;b.parentNode!==(O||c);)b=b.parentNode;return b};(function(){var b=null,c=null,d=null,m= -null,i=null,z={},o={minx:null,miny:null,maxx:null,maxy:null};$(a).mousedown(function(h){if(!(e.spaceKey||h.button===1)){var q=h.button===2;h.altKey&&svgCanvas.cloneSelectedElements(0,0);cb=n.getScreenCTM().inverse();var w=ja(h.pageX,h.pageY,cb),u=w.x*A,t=w.y*A;h.preventDefault();if(q){va="select";Hb=w}w=u/A;t=t/A;var C=eb(h);if(C.tagName==="a"&&C.childNodes.length===1)C=C.firstChild;u=m=c=w;var E=i=d=t;if(v.gridSnapping){w=pa(w);t=pa(t);c=pa(c);d=pa(d)}if(C==wa.selectorParentGroup&&L[0]!=null){C= -h.target;var H=tb(C,"type");if(H=="rotate")va="rotate";else if(H=="resize"){va="resize";Za=tb(C,"dir")}C=L[0]}Ma=C.getAttribute("transform");H=aa(C);if(typeof last_mode=="undefined")last_mode="";if(last_mode=="text"){N.fill="#"+v.initFill.color;N.stroke="#2D2D2D";last_mode=""}else if(last_mode=="fhpath"){N.fill="#"+v.initFill.color;last_mode=""}switch(va){case "select":ra=true;Za="none";if(q)ra=false;if(C!=k){if(L.indexOf(C)==-1){h.shiftKey||Aa(true);db([C]);Gb=C;sa.clear()}if(!q)for(q=0;q<L.length;++q)if(L[q]!= -null){var I=aa(L[q]);I.numberOfItems?I.insertItemBefore(k.createSVGTransform(),0):I.appendItem(k.createSVGTransform())}}else if(!q){Aa();va="multiselect";if(la==null)la=wa.getRubberBandBox();m*=A;i*=A;Da(la,{x:m,y:i,width:0,height:0,display:"inline"},100)}break;case "zoom":ra=true;if(la==null)la=wa.getRubberBandBox();Da(la,{x:u*A,y:u*A,width:0,height:0,display:"inline"},100);break;case "resize":ra=true;c=w;d=t;z=svgedit.utilities.getBBox($("#selectedBox0")[0]);var P={};$.each(z,function(W,Y){P[W]= -Y/A});z=P;q=Va(C)?1:0;if(na(H)){H.insertItemBefore(k.createSVGTransform(),q);H.insertItemBefore(k.createSVGTransform(),q);H.insertItemBefore(k.createSVGTransform(),q)}else{H.appendItem(k.createSVGTransform());H.appendItem(k.createSVGTransform());H.appendItem(k.createSVGTransform());if(svgedit.browser.supportsNonScalingStroke()){if(w=svgedit.browser.isWebkit())I=function(W){var Y=W.getAttributeNS(null,"stroke");W.removeAttributeNS(null,"stroke");setTimeout(function(){W.setAttributeNS(null,"stroke", -Y)},0)};C.style.vectorEffect="non-scaling-stroke";w&&I(C);t=C.getElementsByTagName("*");u=t.length;for(q=0;q<u;q++){t[q].style.vectorEffect="non-scaling-stroke";w&&I(t[q])}}}break;case "fhellipse":case "fhrect":case "fhpath":ra=true;last_mode="fhpath";b=u+","+E+" ";I=N.stroke_width==0?1:N.stroke_width;qa({element:"polyline",curStyles:true,attr:{points:b,id:Ga(),fill:"none",opacity:N.opacity,"stroke-linecap":"round",style:"pointer-events:none"}});o.minx=u;o.maxx=u;o.miny=E;o.maxy=E;break;case "image":ra= -true;I=qa({element:"image",attr:{x:w,y:t,width:0,height:0,id:Ga(),opacity:N.opacity,style:"pointer-events:inherit"}});hb(I,Rb);Jb(I);break;case "square":case "rect":ra=true;c=w;d=t;qa({element:"rect",curStyles:true,attr:{x:w,y:t,width:0,height:0,id:Ga(),opacity:N.opacity,stroke:"#2D2D2D","stroke-width":0,"stroke-opacity":1}});break;case "line":ra=true;I=N.stroke_width==0?2:N.stroke_width;qa({element:"line",curStyles:true,attr:{x1:w,y1:t,x2:w,y2:t,id:Ga(),stroke:"#2D2D2D","stroke-width":I,"stroke-dasharray":N.stroke_dasharray, -"stroke-linejoin":N.stroke_linejoin,"stroke-linecap":N.stroke_linecap,"stroke-opacity":N.stroke_opacity,fill:N.fill,opacity:N.opacity,style:"pointer-events:none"}});break;case "circle":ra=true;qa({element:"circle",curStyles:true,attr:{cx:w,cy:t,r:0,id:Ga(),opacity:N.opacity,stroke:"#2D2D2D","stroke-width":0,"stroke-opacity":1}});break;case "ellipse":ra=true;qa({element:"ellipse",curStyles:true,attr:{cx:w,cy:t,rx:0,ry:0,id:Ga(),opacity:N.opacity,stroke:"#2D2D2D","stroke-width":0,"stroke-opacity":1}}); -break;case "text":ra=true;last_mode="text";qa({element:"text",curStyles:true,attr:{x:w,y:t,id:Ga(),fill:Ua.fill,stroke:"none","stroke-width":0,"font-size":Ua.font_size,"font-family":Ua.font_family,"text-anchor":"middle","xml:space":"preserve",opacity:N.opacity}});break;case "path":case "pathedit":c*=A;d*=A;sa.mouseDown(h,C,c,d);ra=true;break;case "textedit":last_mode="text";c*=A;d*=A;Qa.mouseDown(h,C,c,d);ra=true;break;case "rotate":ra=true;e.undoMgr.beginUndoableChange("transform",L)}h=Ab("mouseDown", -{event:h,start_x:c,start_y:d,selectedElements:L},true);$.each(h,function(W,Y){if(Y&&Y.started)ra=true})}}).mousemove(function(h){if(ra)if(!(h.button===1||e.spaceKey)){var q=L[0],w=ja(h.pageX,h.pageY,cb),u=w.x*A;w=w.y*A;var t=ya($a()),C=x=u/A,E=y=w/A;if(v.gridSnapping){x=pa(x);y=pa(y)}h.preventDefault();switch(va){case "select":if(L[0]!==null){C=x-c;var H=y-d;if(v.gridSnapping){C=pa(C);H=pa(H)}if(h.shiftKey){var I=Ra(c,d,x,y);x=I.x;y=I.y}if(C!=0||H!=0){I=L.length;for(E=0;E<I;++E){q=L[E];if(q==null)break; -var P=k.createSVGTransform();t=aa(q);P.setTranslate(C,H);t.numberOfItems?t.replaceItem(P,0):t.appendItem(P);wa.requestSelector(q).resize()}ia("transition",L)}}break;case "multiselect":C*=A;E*=A;Da(la,{x:Math.min(m,C),y:Math.min(i,E),width:Math.abs(C-m),height:Math.abs(E-i)},100);t=[];C=[];P=Sb();I=L.length;for(E=0;E<I;++E){H=P.indexOf(L[E]);if(H==-1)t.push(L[E]);else P[H]=null}I=P.length;for(E=0;E<I;++E)P[E]&&C.push(P[E]);t.length>0&&e.removeFromSelection(t);C.length>0&&db(C);break;case "resize":t= -aa(q);H=(I=na(t))?z:svgedit.utilities.getBBox(q);E=H.x;P=H.y;var W=H.width,Y=H.height;C=x-c;H=y-d;if(v.gridSnapping){C=pa(C);H=pa(H);Y=pa(Y);W=pa(W)}var R=Va(q);if(R){var T=Math.sqrt(C*C+H*H);H=Math.atan2(H,C)-R*Math.PI/180;C=T*Math.cos(H);H=T*Math.sin(H)}if(Za.indexOf("n")==-1&&Za.indexOf("s")==-1)H=0;if(Za.indexOf("e")==-1&&Za.indexOf("w")==-1)C=0;var V=T=0,ca=Y?(Y+H)/Y:1,Ia=W?(W+C)/W:1;if(Za.indexOf("n")>=0){ca=Y?(Y-H)/Y:1;V=Y}if(Za.indexOf("w")>=0){Ia=W?(W-C)/W:1;T=W}C=k.createSVGTransform(); -H=k.createSVGTransform();W=k.createSVGTransform();if(v.gridSnapping){E=pa(E);T=pa(T);P=pa(P);V=pa(V)}C.setTranslate(-(E+T),-(P+V));if(h.shiftKey)if(Ia==1)Ia=ca;else ca=Ia;H.setScale(Ia,ca);W.setTranslate(E+T,P+V);if(I){I=R?1:0;t.replaceItem(C,2+I);t.replaceItem(H,1+I);t.replaceItem(W,0+I)}else{I=t.numberOfItems;t.replaceItem(W,I-3);t.replaceItem(H,I-2);t.replaceItem(C,I-1)}wa.requestSelector(q).resize();ia("transition",L);break;case "zoom":C*=A;E*=A;Da(la,{x:Math.min(m*A,C),y:Math.min(i*A,E),width:Math.abs(C- -m*A),height:Math.abs(E-i*A)},100);break;case "text":Da(t,{x:x,y:y},1E3);break;case "line":E=null;window.opera||k.suspendRedraw(1E3);if(v.gridSnapping){x=pa(x);y=pa(y)}C=x;I=y;if(h.shiftKey){I=Ra(c,d,C,I);C=I.x;I=I.y}t.setAttributeNS(null,"x2",C);t.setAttributeNS(null,"y2",I);window.opera||k.unsuspendRedraw(E);break;case "foreignObject":case "square":case "rect":case "image":C=Math.abs(x-c);I=Math.abs(y-d);if(va=="square"||h.shiftKey){C=I=Math.max(C,I);E=c<x?c:c-C;P=d<y?d:d-I}else{E=Math.min(c,x); -P=Math.min(d,y)}if(v.gridSnapping){C=pa(C);I=pa(I);E=pa(E);P=pa(P)}Da(t,{width:C,height:I,x:E,y:P},1E3);break;case "circle":I=$(t).attr(["cx","cy"]);C=I.cx;I=I.cy;C=Math.sqrt((x-C)*(x-C)+(y-I)*(y-I));if(v.gridSnapping)C=pa(C);t.setAttributeNS(null,"r",C);break;case "ellipse":I=$(t).attr(["cx","cy"]);C=I.cx;I=I.cy;E=null;window.opera||k.suspendRedraw(1E3);if(v.gridSnapping){x=pa(x);C=pa(C);y=pa(y);I=pa(I)}t.setAttributeNS(null,"rx",Math.abs(x-C));t.setAttributeNS(null,"ry",Math.abs(h.shiftKey?x-C: -y-I));window.opera||k.unsuspendRedraw(E);break;case "fhellipse":case "fhrect":o.minx=Math.min(C,o.minx);o.maxx=Math.max(C,o.maxx);o.miny=Math.min(E,o.miny);o.maxy=Math.max(E,o.maxy);case "fhpath":b+=+C+","+E+" ";t.setAttributeNS(null,"points",b);break;case "path":case "pathedit":x*=A;y*=A;if(v.gridSnapping){x=pa(x);y=pa(y);c=pa(c);d=pa(d)}if(h.shiftKey){if(I=svgedit.path.path){t=I.dragging?I.dragging[0]:c;I=I.dragging?I.dragging[1]:d}else{t=c;I=d}I=Ra(t,I,x,y);x=I.x;y=I.y}if(la&&la.getAttribute("display")!== -"none"){C*=A;E*=A;Da(la,{x:Math.min(m*A,C),y:Math.min(i*A,E),width:Math.abs(C-m*A),height:Math.abs(E-i*A)},100)}sa.mouseMove(x,y);break;case "textedit":x*=A;y*=A;Qa.mouseMove(u,w);break;case "rotate":H=svgedit.utilities.getBBox(q);C=H.x+H.width/2;I=H.y+H.height/2;t=$b(q);t=ja(C,I,t);C=t.x;I=t.y;R=(Math.atan2(I-y,C-x)*(180/Math.PI)-90)%360;if(v.gridSnapping)R=pa(R);if(h.shiftKey)R=Math.round(R/45)*45;e.setRotationAngle(R<-180?360+R:R,true);ia("transition",L)}Ab("mouseMove",{event:h,mouse_x:u,mouse_y:w, -selected:q})}}).click(function(h){h.preventDefault();return false}).dblclick(function(h){var q=h.target.parentNode;if(q!==O){var w=eb(h),u=w.tagName;if(u==="text"&&va!=="textedit"){h=ja(h.pageX,h.pageY,cb);Qa.select(w,h.x,h.y)}if((u==="g"||u==="a")&&Va(w)){Vb(w);w=L[0];Aa(true)}O&&Mb();q.tagName!=="g"&&q.tagName!=="a"||q===B().getCurrentLayer()||w===wa.selectorParentGroup||lc(w)}}).mouseup(function(h){if(h.button!==2){var q=Gb;Gb=null;if(ra){var w=ja(h.pageX,h.pageY,cb),u=w.x*A;w=w.y*A;var t=u/A, -C=w/A,E=ya($a()),H=false;ra=false;switch(va){case "resize":case "multiselect":if(la!=null){la.setAttribute("display","none");fb=[]}va="select";case "select":if(L[0]!=null){if(L[1]==null){u=L[0];switch(u.tagName){case "g":case "use":case "image":case "foreignObject":break;default:Oa.fill=u.getAttribute("fill");Oa.fill_opacity=u.getAttribute("fill-opacity");Oa.stroke=u.getAttribute("stroke");Oa.stroke_opacity=u.getAttribute("stroke-opacity");Oa.stroke_width=u.getAttribute("stroke-width");Oa.stroke_dasharray= -u.getAttribute("stroke-dasharray");Oa.stroke_linejoin=u.getAttribute("stroke-linejoin");Oa.stroke_linecap=u.getAttribute("stroke-linecap")}if(u.tagName=="text"){Ua.font_size=u.getAttribute("font-size");Ua.font_family=u.getAttribute("font-family")}wa.requestSelector(u).showGrips(true)}Kb();if(t!=m||C!=i){h=L.length;for(u=0;u<h;++u){if(L[u]==null)break;L[u].firstChild||wa.requestSelector(L[u]).resize()}}else{u=h.target;if(L[0].nodeName==="path"&&L[1]==null)sa.select(L[0]);else h.shiftKey&&q!=u&&e.removeFromSelection([u])}if(svgedit.browser.supportsNonScalingStroke())if(h= -L[0]){h.removeAttribute("style");svgedit.utilities.walkTree(h,function(W){W.removeAttribute("style")})}}return;case "zoom":la!=null&&la.setAttribute("display","none");ia("zoomed",{x:Math.min(m,t),y:Math.min(i,C),width:Math.abs(t-m),height:Math.abs(C-i),factor:h.shiftKey?0.5:2});return;case "fhpath":q=E.getAttribute("points");t=q.indexOf(",");if(H=t>=0?q.indexOf(",",t+1)>=0:q.indexOf(" ",q.indexOf(" ")+1)>=0)E=sa.smoothPolylineIntoPath(E);break;case "line":q=$(E).attr(["x1","x2","y1","y2"]);H=q.x1!= -q.x2||q.y1!=q.y2;break;case "foreignObject":case "square":case "rect":case "image":q=$(E).attr(["width","height"]);H=q.width!=0||q.height!=0||va==="image";break;case "circle":H=E.getAttribute("r")!=0;break;case "ellipse":q=$(E).attr(["rx","ry"]);H=q.rx!=null||q.ry!=null;break;case "fhellipse":if(o.maxx-o.minx>0&&o.maxy-o.miny>0){E=qa({element:"ellipse",curStyles:true,attr:{cx:(o.minx+o.maxx)/2,cy:(o.miny+o.maxy)/2,rx:(o.maxx-o.minx)/2,ry:(o.maxy-o.miny)/2,id:$a()}});ia("changed",[E]);H=true}break; -case "fhrect":if(o.maxx-o.minx>0&&o.maxy-o.miny>0){E=qa({element:"rect",curStyles:true,attr:{x:o.minx,y:o.miny,width:o.maxx-o.minx,height:o.maxy-o.miny,id:$a()}});ia("changed",[E]);H=true}break;case "text":H=true;ib([E]);Qa.start(E);break;case "path":E=null;ra=true;q=sa.mouseUp(h,E,u,w);E=q.element;H=q.keep;break;case "pathedit":H=true;E=null;sa.mouseUp(h);break;case "textedit":H=false;E=null;Qa.mouseUp(h,u,w);break;case "rotate":H=true;E=null;va="select";q=e.undoMgr.finishUndoableChange();q.isEmpty()|| -oa(q);Kb();ia("changed",L)}u=Ab("mouseUp",{event:h,mouse_x:u,mouse_y:w},true);$.each(u,function(W,Y){if(Y){H=Y.keep||H;E=Y.element;ra=Y.started||ra}});if(!H&&E!=null){B().releaseId($a());E.parentNode.removeChild(E);E=null;for(u=h.target;u.parentNode.parentNode.tagName=="g";)u=u.parentNode;if((va!="path"||!drawn_path)&&u.parentNode.id!="selectorParentGroup"&&u.id!="svgcanvas"&&u.id!="svgroot"){e.setMode("select");ib([u],true)}}else if(E!=null){e.addedNew=true;h=0.2;var I;if(Fb.beginElement&&E.getAttribute("opacity")!= -N.opacity){I=$(Fb).clone().attr({to:N.opacity,dur:h}).appendTo(E);try{I[0].beginElement()}catch(P){}}else h=0;setTimeout(function(){I&&I.remove();E.setAttribute("opacity",N.opacity);E.setAttribute("style","pointer-events:inherit");Ob(E);if(va==="path")sa.toEditMode(E);else v.selectNew&&ib([E],true);oa(new xa(E));ia("changed",[E])},h*1E3)}Ma=null}}});$(a).bind("mousewheel DOMMouseScroll",function(h){if(h.ctrlKey){h.preventDefault();h=h.originalEvent;cb=$("#svgcontent g")[0].getScreenCTM().inverse(); -var q=svgedit.math.transformPoint(h.pageX,h.pageY,cb);q={x:q.x,y:q.y,width:0,height:0};if(h=h.wheelDelta?h.wheelDelta:h.detail?-h.detail:0){q.factor=Math.max(0.75,Math.min(4/3,h));ia("zoomed",q)}}})})();var Jb=function(b){$(b).click(function(c){c.preventDefault()})},Qa=e.textActions=function(){function b(R){var T=w.value==="";$(w).focus();if(!arguments.length)if(T)R=0;else{if(w.selectionEnd!==w.selectionStart)return;R=w.selectionEnd}var V;V=E[R];T||w.setSelectionRange(R,R);u=ya("text_cursor");if(!u){u= -document.createElementNS(s,"line");Da(u,{id:"text_cursor",stroke:"#333","stroke-width":1});u=ya("selectorParentGroup").appendChild(u)}C||(C=setInterval(function(){var ca=u.getAttribute("display")==="none";u.setAttribute("display",ca?"inline":"none")},600));T=z(V.x,H.y);V=z(V.x,H.y+H.height);Da(u,{x1:T.x,y1:T.y,x2:V.x,y2:V.y,visibility:"visible",display:"inline"});t&&t.setAttribute("d","")}function c(R,T,V){if(R===T)b(T);else{V||w.setSelectionRange(R,T);t=ya("text_selectblock");if(!t){t=document.createElementNS(s, -"path");Da(t,{id:"text_selectblock",fill:"green",opacity:0.5,style:"pointer-events:none"});ya("selectorParentGroup").appendChild(t)}R=E[R];var ca=E[T];u.setAttribute("visibility","hidden");T=z(R.x,H.y);V=z(R.x+(ca.x-R.x),H.y);var Ia=z(R.x,H.y+H.height);R=z(R.x+(ca.x-R.x),H.y+H.height);Da(t,{d:"M"+T.x+","+T.y+" L"+V.x+","+V.y+" "+R.x+","+R.y+" "+Ia.x+","+Ia.y+"z",display:"inline"})}}function d(R,T){var V=k.createSVGPoint();V.x=R;V.y=T;if(E.length==1)return 0;V=q.getCharNumAtPosition(V);if(V<0){V=E.length- -2;if(R<=E[0].x)V=0}else if(V>=E.length-2)V=E.length-2;var ca=E[V];R>ca.x+ca.width/2&&V++;return V}function m(R,T,V){var ca=w.selectionStart;R=d(R,T);c(Math.min(ca,R),Math.max(ca,R),!V)}function i(R,T){var V={x:R,y:T};V.x/=A;V.y/=A;if(I){var ca=ja(V.x,V.y,I.inverse());V.x=ca.x;V.y=ca.y}return V}function z(R,T){var V={x:R,y:T};if(I){var ca=ja(V.x,V.y,I);V.x=ca.x;V.y=ca.y}V.x*=A;V.y*=A;return V}function o(R){c(0,q.textContent.length);$(this).unbind(R)}function h(R){if(Y&&q){var T=ja(R.pageX,R.pageY, -cb);T=i(T.x*A,T.y*A);T=d(T.x,T.y);var V=q.textContent,ca=V.substr(0,T).replace(/[a-z0-9]+$/i,"").length;V=V.substr(T).match(/^[a-z0-9]+/i);c(ca,(V?V[0].length:0)+T);$(R.target).click(o);setTimeout(function(){$(R.target).unbind("click",o)},300)}}var q,w,u,t,C,E=[],H,I,P,W,Y;return{select:function(R,T,V){q=R;Qa.toEditMode(T,V)},start:function(R){q=R;Qa.toEditMode()},mouseDown:function(R,T,V,ca){R=i(V,ca);w.focus();b(d(R.x,R.y));P=V;W=ca},mouseMove:function(R,T){var V=i(R,T);m(V.x,V.y)},mouseUp:function(R, -T,V){var ca=i(T,V);m(ca.x,ca.y,true);R.target!==q&&T<P+2&&T>P-2&&V<W+2&&V>W-2&&Qa.toSelectMode(true)},setCursor:b,toEditMode:function(R,T){Y=false;va="textedit";wa.requestSelector(q).showGrips(false);wa.requestSelector(q);Qa.init();$(q).css("cursor","text");if(arguments.length){var V=i(R,T);b(d(V.x,V.y))}else b();setTimeout(function(){Y=true},300)},toSelectMode:function(R){va="select";clearInterval(C);C=null;t&&$(t).attr("display","none");u&&$(u).attr("visibility","hidden");$(q).css("cursor","move"); -if(R){Aa();$(q).css("cursor","move");ia("selected",[q]);db([q],true)}q&&!q.textContent.length&&e.deleteSelectedElements();$(w).blur();q=false},setInputElem:function(R){w=R},clear:function(){va=="textedit"&&Qa.toSelectMode()},init:function(){if(q){if(!q.parentNode){q=L[0];wa.requestSelector(q).showGrips(false)}var R=q.textContent.length,T=q.getAttribute("transform");H=svgedit.utilities.getBBox(q);I=T?$b(q):null;E=Array(R);w.focus();$(q).unbind("dblclick",h).dblclick(h);if(!R)var V={x:H.x+H.width/2, -width:0};for(T=0;T<R;T++){var ca=q.getStartPositionOfChar(T);V=q.getEndPositionOfChar(T);if(!svgedit.browser.supportsGoodTextCharPos()){var Ia=e.contentW*A;ca.x-=Ia;V.x-=Ia;ca.x/=A;V.x/=A}E[T]={x:ca.x,y:H.y,width:V.x-ca.x,height:H.height}}E.push({x:V.x,width:0});c(w.selectionStart,w.selectionEnd,true)}}}}(),sa=e.pathActions=function(){var b=false,c,d,m;svgedit.path.Path.prototype.endChanges=function(o){if(svgedit.browser.isWebkit()){var h=this.elem;h.setAttribute("d",sa.convertPath(h))}o=new Ha(this.elem, -{d:this.last_d},o);oa(o);ia("changed",[this.elem])};svgedit.path.Path.prototype.addPtsToSelection=function(o){$.isArray(o)||(o=[o]);for(var h=0;h<o.length;h++){var q=o[h],w=this.segs[q];w.ptgrip&&this.selected_pts.indexOf(q)==-1&&q>=0&&this.selected_pts.push(q)}this.selected_pts.sort();h=this.selected_pts.length;for(o=Array(h);h--;){w=this.segs[this.selected_pts[h]];w.select(true);o[h]=w.ptgrip}sa.canDeleteNodes=true;sa.closed_subpath=this.subpathIsClosed(this.selected_pts[0]);ia("selected",o)};var i= -c=null,z=false;return{mouseDown:function(o,h,q,w){if(va==="path"){mouse_x=q;mouse_y=w;w=mouse_x/A;h=mouse_y/A;q=ya("path_stretch_line");d=[w,h];if(v.gridSnapping){w=pa(w);h=pa(h);mouse_x=pa(mouse_x);mouse_y=pa(mouse_y)}if(!q){q=document.createElementNS(s,"path");Da(q,{id:"path_stretch_line",stroke:"#22C","stroke-width":"0.5",fill:"none"});q=ya("selectorParentGroup").appendChild(q)}q.setAttribute("display","inline");var u=null;if(i){u=i.pathSegList;for(var t=u.numberOfItems,C=6/A,E=false;t;){t--;var H= -u.getItem(t),I=H.x;H=H.y;if(w>=I-C&&w<=I+C&&h>=H-C&&h<=H+C){E=true;break}}C=$a();svgedit.path.removePath_(C);C=ya(C);I=u.numberOfItems;if(E){if(t<=1&&I>=2){w=u.getItem(0).x;h=u.getItem(0).y;o=q.pathSegList.getItem(1);o=o.pathSegType===4?i.createSVGPathSegLinetoAbs(w,h):i.createSVGPathSegCurvetoCubicAbs(w,h,o.x1/A,o.y1/A,w,h);w=i.createSVGPathSegClosePath();u.appendItem(o);u.appendItem(w)}else if(I<3)return u=false;$(q).remove();element=C;i=null;ra=false;if(b){svgedit.path.path.matrix&&Lb(C,{},svgedit.path.path.matrix.inverse()); -q=C.getAttribute("d");o=$(svgedit.path.path.elem).attr("d");$(svgedit.path.path.elem).attr("d",o+q);$(C).remove();svgedit.path.path.matrix&&svgedit.path.recalcRotatedPath();svgedit.path.path.init();sa.toEditMode(svgedit.path.path.elem);svgedit.path.path.selectPt();return false}}else{if(!$.contains(a,eb(o))){console.log("Clicked outside canvas");return false}u=i.pathSegList.numberOfItems;t=i.pathSegList.getItem(u-1);C=t.x;t=t.y;if(o.shiftKey){o=Ra(C,t,w,h);w=o.x;h=o.y}o=q.pathSegList.getItem(1);o= -o.pathSegType===4?i.createSVGPathSegLinetoAbs(Bb(w),Bb(h)):i.createSVGPathSegCurvetoCubicAbs(Bb(w),Bb(h),o.x1/A,o.y1/A,o.x2/A,o.y2/A);i.pathSegList.appendItem(o);w*=A;h*=A;q.setAttribute("d",["M",w,h,w,h].join(" "));q=u;if(b)q+=svgedit.path.path.segs.length;svgedit.path.addPointGrip(q,w,h)}}else{d_attr="M"+w+","+h+" ";i=qa({element:"path",curStyles:true,attr:{d:d_attr,id:Ga(),opacity:N.opacity}});q.setAttribute("d",["M",mouse_x,mouse_y,mouse_x,mouse_y].join(" "));q=b?svgedit.path.path.segs.length: -0;svgedit.path.addPointGrip(q,mouse_x,mouse_y)}}else if(svgedit.path.path){svgedit.path.path.storeD();C=o.target.id;if(C.substr(0,14)=="pathpointgrip_"){h=svgedit.path.path.cur_pt=parseInt(C.substr(14));svgedit.path.path.dragging=[q,w];u=svgedit.path.path.segs[h];if(o.shiftKey)u.selected?svgedit.path.path.removePtFromSelection(h):svgedit.path.path.addPtsToSelection(h);else{if(svgedit.path.path.selected_pts.length<=1||!u.selected)svgedit.path.path.clearSelection();svgedit.path.path.addPtsToSelection(h)}}else if(C.indexOf("ctrlpointgrip_")== -0){svgedit.path.path.dragging=[q,w];o=C.split("_")[1].split("c");h=o[0]-0;svgedit.path.path.selectPt(h,o[1]-0)}if(!svgedit.path.path.dragging){if(la==null)la=wa.getRubberBandBox();Da(la,{x:q*A,y:w*A,width:0,height:0,display:"inline"},100)}}},mouseMove:function(o,h){z=true;if(va==="path"){if(i){var q=i.pathSegList,w=q.numberOfItems-1;if(d){var u=svgedit.path.addCtrlGrip("1c1"),t=svgedit.path.addCtrlGrip("0c2");u.setAttribute("cx",o);u.setAttribute("cy",h);u.setAttribute("display","inline");u=d[0]; -var C=d[1];q.getItem(w);var E=u+(u-o/A),H=C+(C-h/A);t.setAttribute("cx",E*A);t.setAttribute("cy",H*A);t.setAttribute("display","inline");t=svgedit.path.getCtrlLine(1);Da(t,{x1:o,y1:h,x2:E*A,y2:H*A,display:"inline"});if(w===0)m=[o,h];else{var I=q.getItem(w-1);q=I.x;t=I.y;if(I.pathSegType===6){q+=q-I.x2;t+=t-I.y2}else if(m){q=m[0]/A;t=m[1]/A}svgedit.path.replacePathSeg(6,w,[u,C,q,t,E,H],i)}}else if(u=ya("path_stretch_line")){w=q.getItem(w);if(w.pathSegType===6)svgedit.path.replacePathSeg(6,1,[o,h,(w.x+ -(w.x-w.x2))*A,(w.y+(w.y-w.y2))*A,o,h],u);else m?svgedit.path.replacePathSeg(6,1,[o,h,m[0],m[1],o,h],u):svgedit.path.replacePathSeg(4,1,[o,h],u)}}}else if(svgedit.path.path.dragging){u=svgedit.path.getPointFromGrip({x:svgedit.path.path.dragging[0],y:svgedit.path.path.dragging[1]},svgedit.path.path);C=svgedit.path.getPointFromGrip({x:o,y:h},svgedit.path.path);w=C.x-u.x;u=C.y-u.y;svgedit.path.path.dragging=[o,h];svgedit.path.path.dragctrl?svgedit.path.path.moveCtrl(w,u):svgedit.path.path.movePts(w,u)}else{svgedit.path.path.selected_pts= -[];svgedit.path.path.eachSeg(function(){if(this.next||this.prev){var P=la.getBBox(),W=svgedit.path.getGripPt(this);P=svgedit.math.rectsIntersect(P,{x:W.x,y:W.y,width:0,height:0});this.select(P);P&&svgedit.path.path.selected_pts.push(this.index)}})}},mouseUp:function(o,h){if(va==="path"){d=null;if(!i){h=ya($a());ra=false;m=null}return{keep:true,element:h}}if(svgedit.path.path.dragging){var q=svgedit.path.path.cur_pt;svgedit.path.path.dragging=false;svgedit.path.path.dragctrl=false;svgedit.path.path.update(); -z&&svgedit.path.path.endChanges("Move path point(s)");!o.shiftKey&&!z&&svgedit.path.path.selectPt(q)}else if(la&&la.getAttribute("display")!="none"){la.setAttribute("display","none");la.getAttribute("width")<=2&&la.getAttribute("height")<=2&&sa.toSelectMode(o.target)}else sa.toSelectMode(o.target);z=false},toEditMode:function(o){svgedit.path.path=svgedit.path.getPath_(o);va="pathedit";Aa();svgedit.path.path.show(true).update();svgedit.path.path.oldbbox=svgedit.utilities.getBBox(svgedit.path.path.elem); -b=false},toSelectMode:function(o){var h=o==svgedit.path.path.elem;va="select";svgedit.path.path.show(false);c=false;Aa();svgedit.path.path.matrix&&svgedit.path.recalcRotatedPath();if(h){ia("selected",[o]);db([o],true)}},addSubPath:function(o){if(o){va="path";b=true}else{sa.clear(true);sa.toEditMode(svgedit.path.path.elem)}},select:function(o){if(c===o){sa.toEditMode(o);va="pathedit"}else c=o},reorient:function(){var o=L[0];if(o)if(Va(o)!=0){var h=new ta("Reorient path"),q={d:o.getAttribute("d"),transform:o.getAttribute("transform")}; -h.addSubCommand(new Ha(o,q));Aa();this.resetOrientation(o);oa(h);svgedit.path.getPath_(o).show(false).matrix=null;this.clear();db([o],true);ia("changed",L)}},clear:function(){c=null;if(i){var o=ya($a());$(ya("path_stretch_line")).remove();$(o).remove();$(ya("pathpointgrip_container")).find("*").attr("display","none");i=m=null;ra=false}else va=="pathedit"&&this.toSelectMode();svgedit.path.path&&svgedit.path.path.init().show(false)},resetOrientation:function(o){if(o==null||o.nodeName!="path")return false; -var h=aa(o),q=ka(h).matrix;h.clear();o.removeAttribute("transform");h=o.pathSegList;for(var w=h.numberOfItems,u=0;u<w;++u){var t=h.getItem(u),C=t.pathSegType;if(C!=1){var E=[];$.each(["",1,2],function(H,I){var P=t["x"+I],W=t["y"+I];if(P!==undefined&&W!==undefined){P=ja(P,W,q);E.splice(E.length,0,P.x,P.y)}});svgedit.path.replacePathSeg(C,u,E,o)}}l(o,q)},zoomChange:function(){va=="pathedit"&&svgedit.path.path.update()},getNodePoint:function(){var o=svgedit.path.path.segs[svgedit.path.path.selected_pts.length? -svgedit.path.path.selected_pts[0]:1];return{x:o.item.x,y:o.item.y,type:o.type}},linkControlPoints:function(o){svgedit.path.setLinkControlPoints(o)},clonePathNode:function(){svgedit.path.path.storeD();for(var o=svgedit.path.path.selected_pts,h=o.length,q=[];h--;){var w=o[h];svgedit.path.path.addSeg(w);q.push(w+h);q.push(w+h+1)}svgedit.path.path.init().addPtsToSelection(q);svgedit.path.path.endChanges("Clone path node(s)")},opencloseSubPath:function(){var o=svgedit.path.path.selected_pts;if(o.length=== -1){var h=svgedit.path.path.elem,q=h.pathSegList,w=o[0],u=null,t=null;svgedit.path.path.eachSeg(function(I){if(this.type===2&&I<=w)t=this.item;if(I<=w)return true;if(this.type===2){u=I;return false}else if(this.type===1)return u=false});if(u==null)u=svgedit.path.path.segs.length-1;if(u!==false){var C=h.createSVGPathSegLinetoAbs(t.x,t.y),E=h.createSVGPathSegClosePath();if(u==svgedit.path.path.segs.length-1){q.appendItem(C);q.appendItem(E)}else{svgedit.path.insertItemBefore(h,E,u);svgedit.path.insertItemBefore(h, -C,u)}svgedit.path.path.init().selectPt(u+1)}else if(svgedit.path.path.segs[w].mate){q.removeItem(w);q.removeItem(w);svgedit.path.path.init().selectPt(w-1)}else{for(o=0;o<q.numberOfItems;o++){var H=q.getItem(o);if(H.pathSegType===2)C=o;else if(o===w)q.removeItem(C);else if(H.pathSegType===1&&w<o){E=o-1;q.removeItem(o);break}}for(o=w-C-1;o--;)svgedit.path.insertItemBefore(h,q.getItem(C),E);h=q.getItem(C);svgedit.path.replacePathSeg(2,C,[h.x,h.y]);o=w;svgedit.path.path.init().selectPt(0)}}},deletePathNode:function(){if(sa.canDeleteNodes){svgedit.path.path.storeD(); -for(var o=svgedit.path.path.selected_pts,h=o.length;h--;)svgedit.path.path.deleteSeg(o[h]);var q=function(){var w=svgedit.path.path.elem.pathSegList,u=w.numberOfItems,t=function(H,I){for(;I--;)w.removeItem(H)};if(u<=1)return true;for(;u--;){var C=w.getItem(u);if(C.pathSegType===1){C=w.getItem(u-1);var E=w.getItem(u-2);if(C.pathSegType===2){t(u-1,2);q();break}else if(E.pathSegType===2){t(u-2,3);q();break}}else if(C.pathSegType===2)if(u>0){C=w.getItem(u-1).pathSegType;if(C===2){t(u-1,1);q();break}else if(C=== -1&&w.numberOfItems-1===u){t(u,1);q();break}}}return false};q();if(svgedit.path.path.elem.pathSegList.numberOfItems<=1){sa.toSelectMode(svgedit.path.path.elem);e.deleteSelectedElements()}else{svgedit.path.path.init();svgedit.path.path.clearSelection();if(window.opera){o=$(svgedit.path.path.elem);o.attr("d",o.attr("d"))}svgedit.path.path.endChanges("Delete path node(s)")}}},smoothPolylineIntoPath:function(o){var h=o.points,q=h.numberOfItems;if(q>=4){var w=h.getItem(0),u=null;o=[];o.push(["M",w.x,",", -w.y," C"].join(""));for(var t=1;t<=q-4;t+=3){var C=h.getItem(t),E=h.getItem(t+1),H=h.getItem(t+2);if(u)if((w=svgedit.path.smoothControlPoints(u,C,w))&&w.length==2){C=o[o.length-1].split(",");C[2]=w[0].x;C[3]=w[0].y;o[o.length-1]=C.join(",");C=w[1]}o.push([C.x,C.y,E.x,E.y,H.x,H.y].join(","));w=H;u=E}for(o.push("L");t<q;++t){E=h.getItem(t);o.push([E.x,E.y].join(","))}o=o.join(" ");o=qa({element:"path",curStyles:true,attr:{id:$a(),d:o,fill:"none"}})}return o},setSegType:function(o){svgedit.path.path.setSegType(o)}, -moveNode:function(o,h){var q=svgedit.path.path.selected_pts;if(q.length){svgedit.path.path.storeD();q=svgedit.path.path.segs[q[0]];var w={x:0,y:0};w[o]=h-q.item[o];q.move(w.x,w.y);svgedit.path.path.endChanges("Move path point")}},fixEnd:function(o){for(var h=o.pathSegList,q=h.numberOfItems,w,u=0;u<q;++u){var t=h.getItem(u);if(t.pathSegType===2)w=t;if(t.pathSegType===1){t=h.getItem(u-1);if(t.x!=w.x||t.y!=w.y){h=o.createSVGPathSegLinetoAbs(w.x,w.y);svgedit.path.insertItemBefore(o,h,u);sa.fixEnd(o); -break}}}svgedit.browser.isWebkit()&&o.setAttribute("d",sa.convertPath(o))},convertPath:function(o,h){for(var q=o.pathSegList,w=q.numberOfItems,u=0,t=0,C="",E=null,H=0;H<w;++H){var I=q.getItem(H),P=I.x||0,W=I.y||0,Y=I.x1||0,R=I.y1||0,T=I.x2||0,V=I.y2||0,ca=I.pathSegType,Ia=Ub[ca]["to"+(h?"Lower":"Upper")+"Case"](),Wa=function(kb,ab,bb){ab=ab?" "+ab.join(" "):"";bb=bb?" "+svgedit.units.shortFloat(bb):"";$.each(kb,function(ec,wb){kb[ec]=svgedit.units.shortFloat(wb)});C+=Ia+kb.join(" ")+ab+bb};switch(ca){case 1:C+= -"z";break;case 12:P-=u;case 13:if(h){u+=P;Ia="l"}else{P+=u;u=P;Ia="L"}Wa([[P,t]]);break;case 14:W-=t;case 15:if(h){t+=W;Ia="l"}else{W+=t;t=W;Ia="L"}Wa([[u,W]]);break;case 2:case 4:case 18:P-=u;W-=t;case 5:case 3:if(E&&q.getItem(H-1).pathSegType===1&&!h){u=E[0];t=E[1]}case 19:if(h){u+=P;t+=W}else{P+=u;W+=t;u=P;t=W}if(ca===3)E=[u,t];Wa([[P,W]]);break;case 6:P-=u;Y-=u;T-=u;W-=t;R-=t;V-=t;case 7:if(h){u+=P;t+=W}else{P+=u;Y+=u;T+=u;W+=t;R+=t;V+=t;u=P;t=W}Wa([[Y,R],[T,V],[P,W]]);break;case 8:P-=u;Y-=u; -W-=t;R-=t;case 9:if(h){u+=P;t+=W}else{P+=u;Y+=u;W+=t;R+=t;u=P;t=W}Wa([[Y,R],[P,W]]);break;case 10:P-=u;W-=t;case 11:if(h){u+=P;t+=W}else{P+=u;W+=t;u=P;t=W}Wa([[I.r1,I.r2]],[I.angle,I.largeArcFlag?1:0,I.sweepFlag?1:0],[P,W]);break;case 16:P-=u;T-=u;W-=t;V-=t;case 17:if(h){u+=P;t+=W}else{P+=u;T+=u;W+=t;V+=t;u=P;t=W}Wa([[T,V],[P,W]])}}return C}}}(),fc=this.removeUnusedDefElems=function(){var b=n.getElementsByTagNameNS(s,"defs");if(!b||!b.length)return 0;for(var c=[],d=0,m=["fill","stroke","filter","marker-start", -"marker-mid","marker-end"],i=m.length,z=n.getElementsByTagNameNS(s,"*"),o=z.length,h=0;h<o;h++){for(var q=z[h],w=0;w<i;w++){var u=ba(q.getAttribute(m[w]));u&&c.push(u.substr(1))}(q=gb(q))&&q.indexOf("#")===0&&c.push(q.substr(1))}b=$(b).find("linearGradient, radialGradient, filter, marker, svg, symbol");defelem_ids=[];for(h=b.length;h--;){m=b[h];i=m.id;if(c.indexOf(i)<0){Qb[i]=m;m.parentNode.removeChild(m);d++}}return d};this.svgCanvasToString=function(){for(;fc()>0;);sa.clear(true);$.each(n.childNodes, -function(d,m){d&&m.nodeType===8&&m.data.indexOf("Created with")>=0&&n.insertBefore(m,n.firstChild)});if(O){Mb();ib([O])}var b=[];$(n).find("g:data(gsvg)").each(function(){for(var d=this.attributes,m=d.length,i=0;i<m;i++)if(d[i].nodeName=="id"||d[i].nodeName=="style")m--;if(m<=0){d=this.firstChild;b.push(d);$(this).replaceWith(d)}});var c=this.svgToString(n,0);b.length&&$(b).each(function(){bc(this)});return c};this.svgToString=function(b,c){var d=[],m=svgedit.utilities.toXml,i=v.baseUnit,z=RegExp("^-?[\\d\\.]+"+ -i+"$");if(b){Ob(b);var o=b.attributes,h,q,w=b.childNodes;for(q=0;q<c;q++)d.push(" ");d.push("<");d.push(b.nodeName);if(b.id==="svgcontent"){q=xb();if(i!=="px"){q.w=svgedit.units.convertUnit(q.w,i)+i;q.h=svgedit.units.convertUnit(q.h,i)+i}d.push(' width="'+q.w+'" height="'+q.h+'" xmlns="'+s+'"');var u={};$(b).find("*").andSelf().each(function(){$.each(this.attributes,function(H,I){var P=I.namespaceURI;if(P&&!u[P]&&Ka[P]!=="xmlns"&&Ka[P]!=="xml"){u[P]=true;d.push(" xmlns:"+Ka[P]+'="'+P+'"')}})});q= -o.length;for(i=["width","height","xmlns","x","y","viewBox","id","overflow"];q--;){h=o.item(q);var t=m(h.nodeValue);if(h.nodeName.indexOf("xmlns:")!==0)if(t!=""&&i.indexOf(h.localName)==-1)if(!h.namespaceURI||Ka[h.namespaceURI]){d.push(" ");d.push(h.nodeName);d.push('="');d.push(t);d.push('"')}}}else{if(b.nodeName==="defs"&&!b.firstChild)return;var C=["-moz-math-font-style","_moz-math-font-style"];for(q=o.length-1;q>=0;q--){h=o.item(q);t=m(h.nodeValue);if(!(C.indexOf(h.localName)>=0))if(t!="")if(t.indexOf("pointer-events")!== -0)if(!(h.localName==="class"&&t.indexOf("se_")===0)){d.push(" ");if(h.localName==="d")t=sa.convertPath(b,true);if(isNaN(t)){if(z.test(t))t=svgedit.units.shortFloat(t)+i}else t=svgedit.units.shortFloat(t);if(lb.apply&&b.nodeName==="image"&&h.localName==="href"&&lb.images&&lb.images==="embed"){var E=nb[t];if(E)t=E}if(!h.namespaceURI||h.namespaceURI==s||Ka[h.namespaceURI]){d.push(h.nodeName);d.push('="');d.push(t);d.push('"')}}}}if(b.hasChildNodes()){d.push(">");c++;o=false;for(q=0;q<w.length;q++){i= -w.item(q);switch(i.nodeType){case 1:d.push("\n");d.push(this.svgToString(w.item(q),c));break;case 3:i=i.nodeValue.replace(/^\s+|\s+$/g,"");if(i!=""){o=true;d.push(m(i)+"")}break;case 4:d.push("\n");d.push(Array(c+1).join(" "));d.push("<![CDATA[");d.push(i.nodeValue);d.push("]]\>");break;case 8:d.push("\n");d.push(Array(c+1).join(" "));d.push("<!--");d.push(i.data);d.push("--\>")}}c--;if(!o){d.push("\n");for(q=0;q<c;q++)d.push(" ")}d.push("</");d.push(b.nodeName);d.push(">")}else d.push("/>")}return d.join("")}; -this.embedImage=function(b,c){$(new Image).load(function(){var d=document.createElement("canvas");d.width=this.width;d.height=this.height;d.getContext("2d").drawImage(this,0,0);try{var m=";svgedit_url="+encodeURIComponent(b);m=d.toDataURL().replace(";base64",m+";base64");nb[b]=m}catch(i){nb[b]=false}Rb=b;c&&c(nb[b])}).attr("src",b)};this.setGoodImage=function(b){Rb=b};this.open=function(){};this.save=function(b){Aa();b&&$.extend(lb,b);lb.apply=true;b=this.svgCanvasToString();ia("saved",b)};this.rasterExport= -function(){Aa();var b=[],c={feGaussianBlur:mb.exportNoBlur,foreignObject:mb.exportNoforeignObject,"[stroke-dasharray]":mb.exportNoDashArray},d=$(n);if(!("font"in $("<canvas>")[0].getContext("2d")))c.text=mb.exportNoText;$.each(c,function(m,i){d.find(m).length&&b.push(i)});c=this.svgCanvasToString();ia("exported",{svg:c,issues:b})};this.getSvgString=function(){lb.apply=false;return this.svgCanvasToString()};this.randomizeIds=function(){arguments.length>0&&arguments[0]==false?svgedit.draw.randomizeIds(false, -B()):svgedit.draw.randomizeIds(true,B())};var Wb=this.uniquifyElems=function(b){var c={},d=["filter","linearGradient","pattern","radialGradient","symbol","textPath","use"];svgedit.utilities.walkTree(b,function(h){if(h.nodeType==1){if(h.id){h.id in c||(c[h.id]={elem:null,attrs:[],hrefs:[]});c[h.id].elem=h}$.each(Pb,function(w,u){var t=h.getAttributeNode(u);if(t){var C=svgedit.utilities.getUrlFromAttr(t.value);if(C=C?C.substr(1):null){C in c||(c[C]={elem:null,attrs:[],hrefs:[]});c[C].attrs.push(t)}}}); -var q=svgedit.utilities.getHref(h);if(q&&d.indexOf(h.nodeName)>=0)if(q=q.substr(1)){q in c||(c[q]={elem:null,attrs:[],hrefs:[]});c[q].hrefs.push(h)}}});for(var m in c)if(m){var i=c[m].elem;if(i){b=Ga();i.id=b;i=c[m].attrs;for(var z=i.length;z--;){var o=i[z];o.ownerElement.setAttribute(o.name,"url(#"+b+")")}i=c[m].hrefs;for(z=i.length;z--;)svgedit.utilities.setHref(i[z],"#"+b)}}},Eb=this.setUseData=function(b){var c=$(b);if(b.tagName!=="use")c=c.find("use");c.each(function(){var d=gb(this).substr(1); -if(d=ya(d)){$(this).data("ref",d);if(d.tagName=="symbol"||d.tagName=="svg")$(this).data("symbol",d).data("ref",d)}})},Nb=this.convertGradients=function(b){var c=$(b).find("linearGradient, radialGradient");if(!c.length&&svgedit.browser.isWebkit())c=$(b).find("*").filter(function(){return this.tagName.indexOf("Gradient")>=0});c.each(function(){if($(this).attr("gradientUnits")==="userSpaceOnUse"){var d=$(n).find('[fill="url(#'+this.id+')"],[stroke="url(#'+this.id+')"]');if(d.length)if(d=svgedit.utilities.getBBox(d[0]))if(this.tagName=== -"linearGradient"){var m=$(this).attr(["x1","y1","x2","y2"]),i=this.gradientTransform.baseVal;if(i&&i.numberOfItems>0){var z=ka(i).matrix;i=ja(m.x1,m.y1,z);z=ja(m.x2,m.y2,z);m.x1=i.x;m.y1=i.y;m.x2=z.x;m.y2=z.y;this.removeAttribute("gradientTransform")}$(this).attr({x1:(m.x1-d.x)/d.width,y1:(m.y1-d.y)/d.height,x2:(m.x2-d.x)/d.width,y2:(m.y2-d.y)/d.height});this.removeAttribute("gradientUnits")}}})},Cb=this.convertToGroup=function(b){b||(b=L[0]);var c=$(b),d=new ta,m;if(c.data("gsvg")){d=$(b.firstChild).attr(["x", -"y"]);$(b.firstChild.firstChild).unwrap();$(b).removeData("gsvg");m=aa(b);var i=k.createSVGTransform();i.setTranslate(d.x,d.y);m.appendItem(i);Sa(b);ia("selected",[b])}else if(c.data("symbol")){b=c.data("symbol");m=c.attr("transform");i=c.attr(["x","y"]);var z=b.getAttribute("viewBox");if(z){z=z.split(" ");i.x-=+z[0];i.y-=+z[1]}m+=" translate("+(i.x||0)+","+(i.y||0)+")";i=c.prev();d.addSubCommand(new La(c[0],c[0].nextSibling,c[0].parentNode));c.remove();z=$(n).find("use:data(symbol)").length;c=f.createElementNS(s, -"g");for(var o=b.childNodes,h=0;h<o.length;h++)c.appendChild(o[h].cloneNode(true));if(svgedit.browser.isGecko()){o=$(Ta()).children("linearGradient,radialGradient,pattern").clone();$(c).append(o)}m&&c.setAttribute("transform",m);m=b.parentNode;Wb(c);svgedit.browser.isGecko()&&$(Ta()).append($(c).find("linearGradient,radialGradient,pattern"));c.id=Ga();i.after(c);if(m){if(!z){i=b.nextSibling;m.removeChild(b);d.addSubCommand(new La(b,i,m))}d.addSubCommand(new xa(c))}Eb(c);svgedit.browser.isGecko()? -Nb(Ta()):Nb(c);svgedit.utilities.walkTreePost(c,function(q){try{Sa(q)}catch(w){console.log(w)}});$(c).find("a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use").each(function(){if(!this.id)this.id=Ga()});ib([c]);(b=Vb(c,true))&&d.addSubCommand(b);oa(d)}else console.log("Unexpected element to ungroup:",b)};this.setSvgString=function(b){try{var c=svgedit.utilities.text2xml(b);this.prepareSvg(c);var d=new ta("Change Source"),m=n.nextSibling,i=k.removeChild(n);d.addSubCommand(new La(i, -m,k));n=f.adoptNode?f.adoptNode(c.documentElement):f.importNode(c.documentElement,true);k.appendChild(n);var z=$(n);e.current_drawing_=new svgedit.draw.Drawing(n,F);var o=B().getNonce();o?ia("setnonce",o):ia("unsetnonce");z.find("image").each(function(){var E=this;Jb(E);var H=gb(this);if(H.indexOf("data:")===0){var I=H.match(/svgedit_url=(.*?);/);if(I){var P=decodeURIComponent(I[1]);$(new Image).load(function(){E.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",P)}).attr("src",P)}}e.embedImage(H)}); -z.find("svg").each(function(){if(!$(this).closest("defs").length){Wb(this);var E=this.parentNode;if(E.childNodes.length===1&&E.nodeName==="g"){$(E).data("gsvg",this);E.id=E.id||Ga()}else bc(this)}});svgedit.browser.isGecko()&&z.find("linearGradient, radialGradient, pattern").appendTo(Ta());Eb(z);Nb(z[0]);svgedit.utilities.walkTreePost(n,function(E){try{Sa(E)}catch(H){console.log(H)}});var h={id:"svgcontent",overflow:v.show_outside_canvas?"visible":"hidden"},q=false;if(z.attr("viewBox")){var w=z.attr("viewBox").split(" "); -h.width=w[2];h.height=w[3]}else $.each(["width","height"],function(E,H){var I=z.attr(H);I||(I="100%");if((I+"").substr(-1)==="%")q=true;else h[H]=jb(H,I)});yb();z.children().find("a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use").each(function(){if(!this.id)this.id=Ga()});if(q){var u=getStrokedBBox();h.width=u.width+u.x;h.height=u.height+u.y}if(h.width<=0)h.width=100;if(h.height<=0)h.height=100;z.attr(h);this.contentW=h.width;this.contentH=h.height;d.addSubCommand(new xa(n)); -var t=z.attr(["width","height"]);d.addSubCommand(new Ha(k,t));A=1;svgedit.transformlist.resetListMap();Aa();svgedit.path.clearData();k.appendChild(wa.selectorParentGroup);oa(d);ia("changed",[n])}catch(C){console.log(C);return false}return true};this.importSvgString=function(b){try{var c=svgedit.utilities.encode64(b.length+b).substr(0,32),d=false;if(ob[c])if($(ob[c].symbol).parents("#svgroot").length)d=true;var m=new ta("Import SVG");if(d)var i=ob[c].symbol,z=ob[c].xform;else{var o=svgedit.utilities.text2xml(b); -this.prepareSvg(o);var h;h=f.adoptNode?f.adoptNode(o.documentElement):f.importNode(o.documentElement,true);Wb(h);var q=jb("width",h.getAttribute("width")),w=jb("height",h.getAttribute("height")),u=h.getAttribute("viewBox"),t=u?u.split(" "):[0,0,q,w];for(b=0;b<4;++b)t[b]=+t[b];n.getAttribute("width");var C=+n.getAttribute("height");z=w>q?"scale("+C/3/t[3]+")":"scale("+C/3/t[2]+")";z="translate(0) "+z+" translate(0)";i=f.createElementNS(s,"symbol");var E=Ta();for(svgedit.browser.isGecko()&&$(h).find("linearGradient, radialGradient, pattern").appendTo(E);h.firstChild;)i.appendChild(h.firstChild); -var H=h.attributes;for(h=0;h<H.length;h++){var I=H[h];i.setAttribute(I.nodeName,I.nodeValue)}i.id=Ga();ob[c]={symbol:i,xform:z};Ta().appendChild(i);m.addSubCommand(new xa(i))}var P=f.createElementNS(s,"use");P.id=Ga();hb(P,"#"+i.id);(O||B().getCurrentLayer()).appendChild(P);m.addSubCommand(new xa(P));Aa();P.setAttribute("transform",z);Sa(P);$(P).data("symbol",i).data("ref",i);db([P]);oa(m);ia("changed",[n])}catch(W){console.log(W);return false}return true};var yb=e.identifyLayers=function(){Mb(); -B().identifyLayers()};this.createLayer=function(b){var c=new ta("Create Layer");b=B().createLayer(b);c.addSubCommand(new xa(b));oa(c);Aa();ia("changed",[b])};this.cloneLayer=function(b){var c=new ta("Duplicate Layer"),d=f.createElementNS(s,"g"),m=f.createElementNS(s,"title");m.textContent=b;d.appendChild(m);m=B().getCurrentLayer();$(m).after(d);m=m.childNodes;for(var i=0;i<m.length;i++){var z=m[i];z.localName!="title"&&d.appendChild(vb(z))}Aa();yb();c.addSubCommand(new xa(d));oa(c);e.setCurrentLayer(b); -ia("changed",[d])};this.deleteCurrentLayer=function(){var b=B().getCurrentLayer(),c=b.nextSibling,d=b.parentNode;if(b=B().deleteCurrentLayer()){var m=new ta("Delete Layer");m.addSubCommand(new La(b,c,d));oa(m);Aa();ia("changed",[d]);return true}return false};this.setCurrentLayer=function(b){(b=B().setCurrentLayer(svgedit.utilities.toXml(b)))&&Aa();return b};this.renameCurrentLayer=function(b){var c=B();if(c.current_layer){var d=c.current_layer;if(!e.setCurrentLayer(b)){for(var m=new ta("Rename Layer"), -i=0;i<c.getNumLayers();++i)if(c.all_layers[i][1]==d)break;var z=c.getLayerName(i);c.all_layers[i][0]=svgedit.utilities.toXml(b);var o=d.childNodes.length;for(i=0;i<o;++i){var h=d.childNodes.item(i);if(h&&h.tagName=="title"){for(;h.firstChild;)h.removeChild(h.firstChild);h.textContent=b;m.addSubCommand(new Ha(h,{"#text":z}));oa(m);ia("changed",[d]);return true}}}c.current_layer=d}return false};this.setCurrentLayerPosition=function(b){var c=B();if(c.current_layer&&b>=0&&b<c.getNumLayers()){for(var d= -0;d<c.getNumLayers();++d)if(c.all_layers[d][1]==c.current_layer)break;if(d==c.getNumLayers())return false;if(d!=b){var m=null,i=c.current_layer.nextSibling;if(b>d){if(b<c.getNumLayers()-1)m=c.all_layers[b+1][1]}else m=c.all_layers[b][1];n.insertBefore(c.current_layer,m);oa(new ha(c.current_layer,i,n));yb();e.setCurrentLayer(c.getLayerName(b));return true}}return false};this.setLayerVisibility=function(b,c){var d=B(),m=d.getLayerVisibility(b),i=d.setLayerVisibility(b,c);if(i)oa(new Ha(i,{display:m? -"inline":"none"},"Layer Visibility"));else return false;if(i==d.getCurrentLayer()){Aa();sa.clear()}return true};this.moveSelectedToLayer=function(b){for(var c=null,d=B(),m=0;m<d.getNumLayers();++m)if(d.getLayerName(m)==b){c=d.all_layers[m][1];break}if(!c)return false;b=new ta("Move Elements to Layer");d=L;for(m=d.length;m--;){var i=d[m];if(i){var z=i.nextSibling,o=i.parentNode;c.appendChild(i);b.addSubCommand(new ha(i,z,o))}}oa(b);return true};this.mergeLayer=function(b){var c=new ta("Merge Layer"), -d=B(),m=$(d.current_layer).prev()[0];if(m){for(c.addSubCommand(new La(d.current_layer,d.current_layer.nextSibling,n));d.current_layer.firstChild;){var i=d.current_layer.firstChild;if(i.localName=="title"){c.addSubCommand(new La(i,i.nextSibling,d.current_layer));d.current_layer.removeChild(i)}else{var z=i.nextSibling;m.appendChild(i);c.addSubCommand(new ha(i,z,d.current_layer))}}n.removeChild(d.current_layer);if(!b){Aa();yb();ia("changed",[n]);oa(c)}d.current_layer=m;return c}};this.mergeAllLayers= -function(){var b=new ta("Merge all Layers"),c=B();for(c.current_layer=c.all_layers[c.getNumLayers()-1][1];$(n).children("g").length>1;)b.addSubCommand(e.mergeLayer(true));Aa();yb();ia("changed",[n]);oa(b)};var Mb=this.leaveContext=function(){var b=ub.length;if(b){for(var c=0;c<b;c++){var d=ub[c],m=tb(d,"orig_opac");m!==1?d.setAttribute("opacity",m):d.removeAttribute("opacity");d.setAttribute("style","pointer-events: inherit")}ub=[];Aa(true);ia("contextset",null)}O=null},lc=this.setContext=function(b){Mb(); -if(typeof b==="string")b=ya(b);O=b;$(b).parentsUntil("#svgcontent").andSelf().siblings().each(function(){var c=this.getAttribute("opacity")||1;tb(this,"orig_opac",c);this.setAttribute("opacity",c*0.33);this.setAttribute("style","pointer-events: none");ub.push(this)});Aa();ia("contextset",O)};this.clear=function(){sa.clear();Aa();e.clearSvgContentElement();e.current_drawing_=new svgedit.draw.Drawing(n);e.createLayer("Labels");e.createLayer("Masks");e.undoMgr.resetUndoStack();wa.initGroup();la=wa.getRubberBandBox(); -ia("cleared")};this.linkControlPoints=sa.linkControlPoints;this.getContentElem=function(){return n};this.getRootElem=function(){return k};this.getSelectedElems=function(){return L};var xb=this.getResolution=function(){var b=n.getAttribute("width")/A,c=n.getAttribute("height")/A;return{w:b,h:c,zoom:A}};this.getZoom=function(){return A};this.getVersion=function(){return"svgcanvas.js ($Rev: 2199 $)"};this.setUiStrings=function(b){$.extend(mb,b.notification)};this.setConfig=function(b){$.extend(v,b)}; -this.getTitle=function(b){if(b=b||L[0]){b=$(b).data("gsvg")||$(b).data("symbol")||b;b=b.childNodes;for(var c=0;c<b.length;c++)if(b[c].nodeName=="title")return b[c].textContent;return""}};this.setGroupTitle=function(b){var c=L[0];c=$(c).data("gsvg")||c;var d=$(c).children("title"),m=new ta("Set Label");if(b.length)if(d.length){d=d[0];m.addSubCommand(new Ha(d,{"#text":d.textContent}));d.textContent=b}else{d=f.createElementNS(s,"title");d.textContent=b;$(c).prepend(d);m.addSubCommand(new xa(d))}else{m.addSubCommand(new La(d[0], -d.nextSibling,c));d.remove()}oa(m)};this.getDocumentTitle=function(){return e.getTitle(n)};this.setDocumentTitle=function(b){for(var c=n.childNodes,d=false,m="",i=new ta("Change Image Title"),z=0;z<c.length;z++)if(c[z].nodeName=="title"){d=c[z];m=d.textContent;break}if(!d){d=f.createElementNS(s,"title");n.insertBefore(d,n.firstChild)}if(b.length)d.textContent=b;else d.parentNode.removeChild(d);i.addSubCommand(new Ha(d,{"#text":m}));oa(i)};this.getEditorNS=function(b){b&&n.setAttribute("xmlns:se", -"http://svg-edit.googlecode.com");return"http://svg-edit.googlecode.com"};this.setResolution=function(b,c){var d=xb(),m=d.w;d=d.h;var i;if(b=="fit"){var z=getStrokedBBox();if(z){i=new ta("Fit Canvas to Content");var o=Ib();db(o);var h=[],q=[];$.each(o,function(){h.push(z.x*-1);q.push(z.y*-1)});o=e.moveSelectedElements(h,q,true);i.addSubCommand(o);Aa();b=Math.round(z.width);c=Math.round(z.height)}else return false}if(b!=m||c!=d){o=k.suspendRedraw(1E3);i||(i=new ta("Change Image Dimensions"));b=jb("width", -b);c=jb("height",c);n.setAttribute("width",b);n.setAttribute("height",c);this.contentW=b;this.contentH=c;i.addSubCommand(new Ha(n,{width:m,height:d}));n.setAttribute("viewBox",[0,0,b/A,c/A].join(" "));i.addSubCommand(new Ha(n,{viewBox:["0 0",m,d].join(" ")}));oa(i);k.unsuspendRedraw(o);ia("changed",[n])}return true};this.getOffset=function(){return $(n).attr(["x","y"])};this.setBBoxZoom=function(b,c,d){var m=0.85,i=function(z){if(!z)return false;var o=Math.min(Math.round(c/z.width*100*m)/100,Math.round(d/ -z.height*100*m)/100);e.setZoom(o);return{zoom:o,bbox:z}};if(typeof b=="object"){b=b;if(b.width==0||b.height==0){e.setZoom(b.zoom?b.zoom:A*b.factor);return{zoom:A,bbox:b}}return i(b)}switch(b){case "selection":if(!L[0])return;b=$.map(L,function(z){if(z)return z});b=getStrokedBBox(b);break;case "canvas":b=xb();m=0.95;b={width:b.w,height:b.h,x:0,y:0};break;case "content":b=getStrokedBBox();break;case "layer":b=getStrokedBBox(Ib(B().getCurrentLayer()));break;default:return}return i(b)};this.setZoom=function(b){var c= -xb();n.setAttribute("viewBox","0 0 "+c.w/b+" "+c.h/b);A=b;$.each(L,function(d,m){m&&wa.requestSelector(m).resize()});sa.zoomChange();Ab("zoomChanged",b)};this.getMode=function(){return va};this.setMode=function(b){sa.clear(true);Qa.clear();Oa=L[0]&&L[0].nodeName=="text"?Ua:N;va=b};this.getColor=function(b){return Oa[b]};this.setColor=function(b,c,d){N[b]=c;Oa[b+"_paint"]={type:"solidColor"};for(var m=[],i=L.length;i--;){var z=L[i];if(z)if(z.tagName=="g")svgedit.utilities.walkTree(z,function(o){o.nodeName!= -"g"&&m.push(o)});else if(b=="fill")z.tagName!="polyline"&&z.tagName!="line"&&m.push(z);else m.push(z)}if(m.length>0)if(d)pb(b,c,m);else{Pa(b,c,m);ia("changed",m)}};var Ta=function(){var b=n.getElementsByTagNameNS(s,"defs");if(b.length>0)b=b[0];else{b=f.createElementNS(s,"defs");n.firstChild?n.insertBefore(b,n.firstChild.nextSibling):n.appendChild(b)}return b},hc=this.setGradient=function(b){if(!(!Oa[b+"_paint"]||Oa[b+"_paint"].type=="solidColor")){var c=e[b+"Grad"],d=gc(c),m=Ta();if(d)c=d;else{c= -m.appendChild(f.importNode(c,true));c.id=Ga()}e.setColor(b,"url(#"+c.id+")")}},gc=function(b){var c=Ta();c=$(c).find("linearGradient, radialGradient");for(var d=c.length,m=["r","cx","cy","fx","fy"];d--;){var i=c[d];if(b.tagName=="linearGradient"){if(b.getAttribute("x1")!=i.getAttribute("x1")||b.getAttribute("y1")!=i.getAttribute("y1")||b.getAttribute("x2")!=i.getAttribute("x2")||b.getAttribute("y2")!=i.getAttribute("y2"))continue}else{var z=$(b).attr(m),o=$(i).attr(m),h=false;$.each(m,function(E, -H){if(z[H]!=o[H])h=true});if(h)continue}var q=b.getElementsByTagNameNS(s,"stop"),w=i.getElementsByTagNameNS(s,"stop");if(q.length==w.length){for(var u=q.length;u--;){var t=q[u],C=w[u];if(t.getAttribute("offset")!=C.getAttribute("offset")||t.getAttribute("stop-opacity")!=C.getAttribute("stop-opacity")||t.getAttribute("stop-color")!=C.getAttribute("stop-color"))break}if(u==-1)return i}}return null};this.setPaint=function(b,c){var d=new $.jGraduate.Paint(c);this.setPaintOpacity(b,d.alpha/100,true);Oa[b+ -"_paint"]=d;switch(d.type){case "solidColor":this.setColor(b,d.solidColor!="none"?"#"+d.solidColor:"none");break;case "linearGradient":case "radialGradient":e[b+"Grad"]=d[d.type];hc(b)}};this.getStrokeWidth=function(){return Oa.stroke_width};this.setStrokeWidth=function(b){if(b==0&&["line","path"].indexOf(va)>=0)e.setStrokeWidth(1);else{Oa.stroke_width=b;for(var c=[],d=L.length;d--;){var m=L[d];if(m)m.tagName=="g"?svgedit.utilities.walkTree(m,function(i){i.nodeName!="g"&&c.push(i)}):c.push(m)}if(c.length> -0){Pa("stroke-width",b,c);ia("changed",L)}}};this.setStrokeAttr=function(b,c){N[b.replace("-","_")]=c;for(var d=[],m=L.length;m--;){var i=L[m];if(i)i.tagName=="g"?svgedit.utilities.walkTree(i,function(z){z.nodeName!="g"&&d.push(z)}):d.push(i)}if(d.length>0){Pa(b,c,d);ia("changed",L)}};this.getStyle=function(){return N};this.getOpacity=function(){return N.opacity};this.setOpacity=function(b){N.opacity=b;Pa("opacity",b)};this.getFillOpacity=function(){return N.fill_opacity};this.getStrokeOpacity=function(){return N.stroke_opacity}; -this.setPaintOpacity=function(b,c,d){N[b+"_opacity"]=c;d?pb(b+"-opacity",c):Pa(b+"-opacity",c)};this.getBlur=function(b){var c=0;if(b)if(b.getAttribute("filter"))if(b=ya(b.id+"_blur"))c=b.firstChild.getAttribute("stdDeviation");return c};(function(){function b(){var i=e.undoMgr.finishUndoableChange();c.addSubCommand(i);oa(c);d=c=null}var c=null,d=null,m=false;e.setBlurNoUndo=function(i){if(d)if(i===0){pb("filter","");m=true}else{var z=L[0];m&&pb("filter","url(#"+z.id+"_blur)");if(svgedit.browser.isWebkit()){console.log("e", -z);z.removeAttribute("filter");z.setAttribute("filter","url(#"+z.id+"_blur)")}pb("stdDeviation",i,[d.firstChild]);e.setBlurOffsets(d,i)}else e.setBlur(i)};e.setBlurOffsets=function(i,z){if(z>3)Da(i,{x:"-50%",y:"-50%",width:"200%",height:"200%"},100);else if(!svgedit.browser.isWebkit()){i.removeAttribute("x");i.removeAttribute("y");i.removeAttribute("width");i.removeAttribute("height")}};e.setBlur=function(i,z){if(c)b();else{var o=L[0],h=o.id;d=ya(h+"_blur");i-=0;var q=new ta;if(d){if(i===0)d=null}else{var w= -qa({element:"feGaussianBlur",attr:{"in":"SourceGraphic",stdDeviation:i}});d=qa({element:"filter",attr:{id:h+"_blur"}});d.appendChild(w);Ta().appendChild(d);q.addSubCommand(new xa(d))}w={filter:o.getAttribute("filter")};if(i===0){o.removeAttribute("filter");q.addSubCommand(new Ha(o,w))}else{Pa("filter","url(#"+h+"_blur)");q.addSubCommand(new Ha(o,w));e.setBlurOffsets(d,i);c=q;e.undoMgr.beginUndoableChange("stdDeviation",[d?d.firstChild:null]);if(z){e.setBlurNoUndo(i);b()}}}}})();this.getBold=function(){var b= -L[0];if(b!=null&&b.tagName=="text"&&L[1]==null)return b.getAttribute("font-weight")=="bold";return false};this.setBold=function(b){var c=L[0];if(c!=null&&c.tagName=="text"&&L[1]==null)Pa("font-weight",b?"bold":"normal");L[0].textContent||Qa.setCursor()};this.getItalic=function(){var b=L[0];if(b!=null&&b.tagName=="text"&&L[1]==null)return b.getAttribute("font-style")=="italic";return false};this.setItalic=function(b){var c=L[0];if(c!=null&&c.tagName=="text"&&L[1]==null)Pa("font-style",b?"italic":"normal"); -L[0].textContent||Qa.setCursor()};this.getFontFamily=function(){return Ua.font_family};this.setFontFamily=function(b){Ua.font_family=b;Pa("font-family",b);L[0]&&!L[0].textContent&&Qa.setCursor()};this.setFontColor=function(b){Ua.fill=b;Pa("fill",b)};this.getFontSize=function(){return Ua.fill};this.getFontSize=function(){return Ua.font_size};this.setFontSize=function(b){Ua.font_size=b;Pa("font-size",b);L[0].textContent||Qa.setCursor()};this.getText=function(){var b=L[0];if(b==null)return"";return b.textContent}; -this.setTextContent=function(b){Pa("#text",b);Qa.init(b);Qa.setCursor()};this.setImageURL=function(b){var c=L[0];if(c){var d=$(c).attr(["width","height"]);d=!d.width||!d.height;var m=gb(c);if(m!==b)d=true;else if(!d)return;var i=new ta("Change Image URL");hb(c,b);i.addSubCommand(new Ha(c,{"#href":m}));d?$(new Image).load(function(){var z=$(c).attr(["width","height"]);$(c).attr({width:this.width,height:this.height});wa.requestSelector(c).resize();i.addSubCommand(new Ha(c,z));oa(i);ia("changed",[c])}).attr("src", -b):oa(i)}};this.setLinkURL=function(b){var c=L[0];if(c){if(c.tagName!=="a"){c=$(c).parents("a");if(c.length)c=c[0];else return}var d=gb(c);if(d!==b){var m=new ta("Change Link URL");hb(c,b);m.addSubCommand(new Ha(c,{"#href":d}));oa(m)}}};this.setRectRadius=function(b){var c=L[0];if(c!=null&&c.tagName=="rect"){var d=c.getAttribute("rx");if(d!=b){c.setAttribute("rx",b);c.setAttribute("ry",b);oa(new Ha(c,{rx:d,ry:d},"Radius"));ia("changed",[c])}}};this.makeHyperlink=function(b){e.groupSelectedElements("a", -b)};this.removeHyperlink=function(){e.ungroupSelectedElement()};this.setSegType=function(b){sa.setSegType(b)};this.convertToPath=function(b,c){if(b==null)$.each(L,function(Y,R){R&&e.convertToPath(R)});else{if(!c)var d=new ta("Convert element to Path");var m=c?{}:{fill:N.fill,"fill-opacity":N.fill_opacity,stroke:N.stroke,"stroke-width":N.stroke_width,"stroke-dasharray":N.stroke_dasharray,"stroke-linejoin":N.stroke_linejoin,"stroke-linecap":N.stroke_linecap,"stroke-opacity":N.stroke_opacity,opacity:N.opacity, -visibility:"hidden"};$.each(["marker-start","marker-end","marker-mid","filter","clip-path"],function(){if(b.getAttribute(this))m[this]=b.getAttribute(this)});var i=qa({element:"path",attr:m}),z=b.getAttribute("transform");z&&i.setAttribute("transform",z);var o=b.id,h=b.parentNode;b.nextSibling?h.insertBefore(i,b):h.appendChild(i);var q="",w=function(Y){$.each(Y,function(R,T){var V=T[1];q+=T[0];for(var ca=0;ca<V.length;ca+=2)q+=V[ca]+","+V[ca+1]+" "})},u=1.81;switch(b.tagName){case "ellipse":case "circle":var t= -$(b).attr(["rx","ry","cx","cy"]),C=t.cx,E=t.cy,H=t.rx;t=t.ry;if(b.tagName=="circle")H=t=$(b).attr("r");w([["M",[C-H,E]],["C",[C-H,E-t/u,C-H/u,E-t,C,E-t]],["C",[C+H/u,E-t,C+H,E-t/u,C+H,E]],["C",[C+H,E+t/u,C+H/u,E+t,C,E+t]],["C",[C-H/u,E+t,C-H,E+t/u,C-H,E]],["Z",[]]]);break;case "path":q=b.getAttribute("d");break;case "line":t=$(b).attr(["x1","y1","x2","y2"]);q="M"+t.x1+","+t.y1+"L"+t.x2+","+t.y2;break;case "polyline":case "polygon":q="M"+b.getAttribute("points");break;case "rect":t=$(b).attr(["rx", -"ry"]);H=t.rx;t=t.ry;var I=b.getBBox();C=I.x;E=I.y;var P=I.width;I=I.height;u=4-u;!H&&!t?w([["M",[C,E]],["L",[C+P,E]],["L",[C+P,E+I]],["L",[C,E+I]],["L",[C,E]],["Z",[]]]):w([["M",[C,E+t]],["C",[C,E+t/u,C+H/u,E,C+H,E]],["L",[C+P-H,E]],["C",[C+P-H/u,E,C+P,E+t/u,C+P,E+t]],["L",[C+P,E+I-t]],["C",[C+P,E+I-t/u,C+P-H/u,E+I,C+P-H,E+I]],["L",[C+H,E+I]],["C",[C+H/u,E+I,C,E+I-t/u,C,E+I-t]],["L",[C,E+t]],["Z",[]]]);break;default:i.parentNode.removeChild(i)}q&&i.setAttribute("d",q);if(c){sa.resetOrientation(i); -d=false;try{d=i.getBBox()}catch(W){}i.parentNode.removeChild(i);return d}else{if(z){z=aa(i);na(z)&&sa.resetOrientation(i)}d.addSubCommand(new La(b,b.nextSibling,h));d.addSubCommand(new xa(i));Aa();b.parentNode.removeChild(b);i.setAttribute("id",o);i.removeAttribute("visibility");db([i],true);oa(d)}}};var pb=function(b,c,d){var m=k.suspendRedraw(1E3);va=="pathedit"&&sa.moveNode(b,c);d=d||L;for(var i=d.length,z=["g","polyline","path"],o=["transform","opacity","filter"];i--;){var h=d[i];if(h!=null){va=== -"textedit"&&b!=="#text"&&h.textContent.length&&Qa.toSelectMode(h);if((b==="x"||b==="y")&&z.indexOf(h.tagName)>=0){var q=getStrokedBBox([h]);e.moveSelectedElements((b==="x"?c-q.x:0)*A,(b==="y"?c-q.y:0)*A,true)}else{h.tagName==="g"&&o.indexOf(b);q=b==="#text"?h.textContent:h.getAttribute(b);if(q==null)q="";if(q!==String(c)){if(b=="#text"){svgedit.utilities.getBBox(h);h.textContent=c;if(/rotate/.test(h.getAttribute("transform")))h=Tb(h)}else b=="#href"?hb(h,c):h.setAttribute(b,c);if(svgedit.browser.isGecko()&& -h.nodeName==="text"&&/rotate/.test(h.getAttribute("transform")))if((c+"").indexOf("url")===0||["font-size","font-family","x","y"].indexOf(b)>=0&&h.textContent)h=Tb(h);L.indexOf(h)>=0&&setTimeout(function(){h.parentNode&&wa.requestSelector(h).resize()},0);q=Va(h);if(q!=0&&b!="transform")for(var w=aa(h),u=w.numberOfItems;u--;)if(w.getItem(u).type==4){w.removeItem(u);var t=svgedit.utilities.getBBox(h),C=ja(t.x+t.width/2,t.y+t.height/2,ka(w).matrix);t=C.x;C=C.y;var E=k.createSVGTransform();E.setRotate(q, -t,C);w.insertItemBefore(E,u);break}}}}}k.unsuspendRedraw(m)},Pa=this.changeSelectedAttribute=function(b,c,d){d=d||L;e.undoMgr.beginUndoableChange(b,d);pb(b,c,d);b=e.undoMgr.finishUndoableChange();b.isEmpty()||oa(b)};this.deleteSelectedElements=function(){for(var b=new ta("Delete Elements"),c=L.length,d=[],m=0;m<c;++m){var i=L[m];if(i==null)break;var z=i.parentNode,o=i;wa.releaseSelector(o);svgedit.path.removePath_(o.id);if(z.tagName==="a"&&z.childNodes.length===1){o=z;z=z.parentNode}var h=o.nextSibling; -o=z.removeChild(o);d.push(i);L[m]=null;b.addSubCommand(new La(o,h,z))}b.isEmpty()||oa(b);ia("changed",d);Aa()};this.cutSelectedElements=function(){for(var b=new ta("Cut Elements"),c=L.length,d=[],m=0;m<c;++m){var i=L[m];if(i==null)break;var z=i.parentNode,o=i;wa.releaseSelector(o);svgedit.path.removePath_(o.id);var h=o.nextSibling;o=z.removeChild(o);d.push(i);L[m]=null;b.addSubCommand(new La(o,h,z))}b.isEmpty()||oa(b);ia("changed",d);Aa();e.clipBoard=d};this.copySelectedElements=function(){e.clipBoard= -$.merge([],L)};this.pasteElements=function(b,c,d){var m=e.clipBoard,i=m.length;if(i){for(var z=[],o=new ta("Paste elements");i--;){var h=m[i];if(h){var q=vb(h);if(!ya(h.id))q.id=h.id;z.push(q);(O||B().getCurrentLayer()).appendChild(q);o.addSubCommand(new xa(q))}}ib(z);if(b!=="in_place"){var w,u;if(b){if(b==="point"){w=c;u=d}}else{w=Hb.x;u=Hb.y}b=getStrokedBBox(z);var t=w-(b.x+b.width/2),C=u-(b.y+b.height/2),E=[],H=[];$.each(z,function(){E.push(t);H.push(C)});w=e.moveSelectedElements(E,H,false);o.addSubCommand(w)}oa(o); -ia("changed",z)}};this.groupSelectedElements=function(b){b||(b="g");var c="";switch(b){case "a":c="Make hyperlink";var d="";if(arguments.length>1)d=arguments[1];break;default:b="g";c="Group Elements"}c=new ta(c);var m=qa({element:b,attr:{id:Ga()}});b==="a"&&hb(m,d);c.addSubCommand(new xa(m));for(d=L.length;d--;){var i=L[d];if(i!=null){if(i.parentNode.tagName==="a"&&i.parentNode.childNodes.length===1)i=i.parentNode;var z=i.nextSibling,o=i.parentNode;m.appendChild(i);c.addSubCommand(new ha(i,z,o))}}c.isEmpty()|| -oa(c);ib([m],true)};var Vb=this.pushGroupProperties=function(b,c){var d=b.childNodes,m=d.length,i=b.getAttribute("transform"),z=aa(b),o=ka(z).matrix,h=new ta("Push group properties"),q=0,w=Va(b),u=$(b).attr(["filter","opacity"]),t,C;for(q=0;q<m;q++){var E=d[q];if(E.nodeType===1){if(u.opacity!==null&&u.opacity!==1){E.getAttribute("opacity");var H=Math.round((E.getAttribute("opacity")||1)*u.opacity*100)/100;Pa("opacity",H,[E])}if(u.filter){var I=H=this.getBlur(E);C||(C=this.getBlur(b));if(H)H=C-0+(H- -0);else if(H===0)H=C;if(I)t=rb(E.getAttribute("filter"));else if(t){t=vb(t);Ta().appendChild(t)}else t=rb(u.filter);t.id=E.id+"_"+(t.firstChild.tagName==="feGaussianBlur"?"blur":"filter");Pa("filter","url(#"+t.id+")",[E]);if(H){Pa("stdDeviation",H,[t.firstChild]);e.setBlurOffsets(t,H)}}H=aa(E);if(~E.tagName.indexOf("Gradient"))H=null;if(H)if(E.tagName!=="defs")if(z.numberOfItems){if(w&&z.numberOfItems==1){var P=z.getItem(0).matrix,W=k.createSVGMatrix();if(I=Va(E))W=H.getItem(0).matrix;var Y=svgedit.utilities.getBBox(E), -R=ka(H).matrix,T=ja(Y.x+Y.width/2,Y.y+Y.height/2,R);Y=w+I;R=k.createSVGTransform();R.setRotate(Y,T.x,T.y);P=da(P,W,R.matrix.inverse());I&&H.removeItem(0);if(Y)H.numberOfItems?H.insertItemBefore(R,0):H.appendItem(R);if(P.e||P.f){I=k.createSVGTransform();I.setTranslate(P.e,P.f);H.numberOfItems?H.insertItemBefore(I,0):H.appendItem(I)}}else{I=E.getAttribute("transform");P={};P.transform=I?I:"";I=k.createSVGTransform();P=ka(H).matrix;W=P.inverse();P=da(W,o,P);I.setMatrix(P);H.appendItem(I)}(E=Sa(E))&& -h.addSubCommand(E)}}}if(i){P={};P.transform=i;b.setAttribute("transform","");b.removeAttribute("transform");h.addSubCommand(new Ha(b,P))}if(c&&!h.isEmpty())return h};this.ungroupSelectedElement=function(){var b=L[0];if($(b).data("gsvg")||$(b).data("symbol"))Cb(b);else if(b.tagName==="use"){var c=ya(gb(b).substr(1));$(b).data("symbol",c).data("ref",c);Cb(b)}else{c=$(b).parents("a");if(c.length)b=c[0];if(b.tagName==="g"||b.tagName==="a"){c=new ta("Ungroup Elements");var d=Vb(b,true);d&&c.addSubCommand(d); -d=b.parentNode;for(var m=b.nextSibling,i=Array(b.childNodes.length),z=0;b.firstChild;){var o=b.firstChild,h=o.nextSibling,q=o.parentNode;if(o.tagName==="title"){c.addSubCommand(new La(o,o.nextSibling,q));q.removeChild(o)}else{i[z++]=o=d.insertBefore(o,m);c.addSubCommand(new ha(o,h,q))}}Aa();m=b.nextSibling;b=d.removeChild(b);c.addSubCommand(new La(b,m,d));c.isEmpty()||oa(c);db(i)}}};this.moveToTopSelectedElement=function(){var b=L[0];if(b!=null){b=b;var c=b.parentNode,d=b.nextSibling;b=b.parentNode.appendChild(b); -if(d!=b.nextSibling){oa(new ha(b,d,c,"top"));ia("changed",[b])}}};this.moveToBottomSelectedElement=function(){var b=L[0];if(b!=null){b=b;var c=b.parentNode,d=b.nextSibling,m=b.parentNode.firstChild;if(m.tagName=="title")m=m.nextSibling;if(m.tagName=="defs")m=m.nextSibling;b=b.parentNode.insertBefore(b,m);if(d!=b.nextSibling){oa(new ha(b,d,c,"bottom"));ia("changed",[b])}}};this.moveUpDownSelected=function(b){var c=L[0];if(c){fb=[];var d,m,i=$(Sb(getStrokedBBox([c]))).toArray();b=="Down"&&i.reverse(); -$.each(i,function(){if(m){d=this;return false}else if(this==c)m=true});if(d){i=c.parentNode;var z=c.nextSibling;$(d)[b=="Down"?"before":"after"](c);if(z!=c.nextSibling){oa(new ha(c,z,i,"Move "+b));ia("changed",[c])}}}};this.moveSelectedElements=function(b,c,d){if(b.constructor!=Array){b/=A;c/=A}d=d||true;for(var m=new ta("position"),i=L.length;i--;){var z=L[i];if(z!=null){var o=k.createSVGTransform(),h=aa(z);b.constructor==Array?o.setTranslate(b[i],c[i]):o.setTranslate(b,c);h.numberOfItems?h.insertItemBefore(o, -0):h.appendItem(o);(o=Sa(z))&&m.addSubCommand(o);wa.requestSelector(z).resize()}}if(!m.isEmpty()){d&&oa(m);ia("changed",L);return m}};this.cloneSelectedElements=function(b,c){for(var d=new ta("Clone Elements"),m=L.length,i=0;i<m;++i){var z=L[i];if(z==null)break}m=L.slice(0,i);this.clearSelection(true);for(i=m.length;i--;){z=m[i]=vb(m[i]);(O||B().getCurrentLayer()).appendChild(z);d.addSubCommand(new xa(z))}if(!d.isEmpty()){db(m.reverse());this.moveSelectedElements(b,c,false);oa(d)}};this.alignSelectedElements= -function(b,c){var d=[],m=Number.MAX_VALUE,i=Number.MIN_VALUE,z=Number.MAX_VALUE,o=Number.MIN_VALUE,h=Number.MIN_VALUE,q=Number.MIN_VALUE,w=L.length;if(w){for(var u=0;u<w;++u){if(L[u]==null)break;d[u]=getStrokedBBox([L[u]]);switch(c){case "smallest":if((b=="l"||b=="c"||b=="r")&&(h==Number.MIN_VALUE||h>d[u].width)||(b=="t"||b=="m"||b=="b")&&(q==Number.MIN_VALUE||q>d[u].height)){m=d[u].x;z=d[u].y;i=d[u].x+d[u].width;o=d[u].y+d[u].height;h=d[u].width;q=d[u].height}break;case "largest":if((b=="l"||b== -"c"||b=="r")&&(h==Number.MIN_VALUE||h<d[u].width)||(b=="t"||b=="m"||b=="b")&&(q==Number.MIN_VALUE||q<d[u].height)){m=d[u].x;z=d[u].y;i=d[u].x+d[u].width;o=d[u].y+d[u].height;h=d[u].width;q=d[u].height}break;default:if(d[u].x<m)m=d[u].x;if(d[u].y<z)z=d[u].y;if(d[u].x+d[u].width>i)i=d[u].x+d[u].width;if(d[u].y+d[u].height>o)o=d[u].y+d[u].height}}if(c=="page"){z=m=0;i=e.contentW;o=e.contentH}h=Array(w);q=Array(w);for(u=0;u<w;++u){if(L[u]==null)break;var t=d[u];h[u]=0;q[u]=0;switch(b){case "l":h[u]=m- -t.x;break;case "c":h[u]=(m+i)/2-(t.x+t.width/2);break;case "r":h[u]=i-(t.x+t.width);break;case "t":q[u]=z-t.y;break;case "m":q[u]=(z+o)/2-(t.y+t.height/2);break;case "b":q[u]=o-(t.y+t.height)}}this.moveSelectedElements(h,q)}};this.contentW=xb().w;this.contentH=xb().h;this.updateCanvas=function(b,c){k.setAttribute("width",b);k.setAttribute("height",c);var d=$("#canvasBackground")[0],m=n.getAttribute("x"),i=n.getAttribute("y"),z=b/2-this.contentW*A/2,o=c/2-this.contentH*A/2;Da(n,{width:this.contentW* -A,height:this.contentH*A,x:z,y:o,viewBox:"0 0 "+this.contentW+" "+this.contentH});Da(d,{width:n.getAttribute("width"),height:n.getAttribute("height"),x:z,y:o});(d=ya("background_image"))&&Da(d,{width:"100%",height:"100%"});wa.selectorParentGroup.setAttribute("transform","translate("+z+","+o+")");return{x:z,y:o,old_x:m,old_y:i,d_x:z-m,d_y:o-i}};this.setBackground=function(b,c){var d=ya("canvasBackground"),m=$(d).find("rect")[0],i=ya("background_image");m.setAttribute("fill",b);if(c){if(!i){i=f.createElementNS(s, -"image");Da(i,{id:"background_image",width:"100%",height:"100%",preserveAspectRatio:"xMinYMin",style:"pointer-events:none"})}hb(i,c);d.appendChild(i)}else i&&i.parentNode.removeChild(i)};this.cycleElement=function(b){var c=L[0],d=false,m=Ib(O||B().getCurrentLayer());if(m.length){if(c==null){b=b?m.length-1:0;d=m[b]}else for(var i=m.length;i--;)if(m[i]==c){b=b?i-1:i+1;if(b>=m.length)b=0;else if(b<0)b=m.length-1;d=m[b];break}ib([d],true);ia("selected",L)}};this.clear();this.getPrivateMethods=function(){return{addCommandToHistory:oa, -setGradient:hc,addSvgElementFromJson:qa,assignAttributes:Da,BatchCommand:ta,call:ia,ChangeElementCommand:Ha,copyElem:vb,ffClone:Tb,findDefs:Ta,findDuplicateGradient:gc,getElem:ya,getId:$a,getIntersectionList:Sb,getMouseTarget:eb,getNextId:Ga,getPathBBox:ac,getUrlFromAttr:ba,hasMatrixTransform:na,identifyLayers:yb,InsertElementCommand:xa,isIdentity:svgedit.math.isIdentity,logMatrix:cc,matrixMultiply:da,MoveElementCommand:ha,preventClickDefault:Jb,recalculateAllSelectedDimensions:Kb,recalculateDimensions:Sa, -remapElement:Lb,RemoveElementCommand:La,removeUnusedDefElems:fc,round:Bb,runExtensions:Ab,sanitizeSvg:sb,SVGEditTransformList:svgedit.transformlist.SVGTransformList,toString:toString,transformBox:svgedit.math.transformBox,transformListToTransform:ka,transformPoint:ja,walkTree:svgedit.utilities.walkTree}}};(function(){document.addEventListener("touchstart",touchHandler,true);document.addEventListener("touchmove",touchHandler,true);document.addEventListener("touchend",touchHandler,true);document.addEventListener("touchcancel",touchHandler,true);if(!window.svgEditor)window.svgEditor=function(a){function J(B,A){var O=l.setSvgString(B)!==false;A=A||a.noop;O?A(true):a.alert(k.notification.errorLoadingSVG,function(){A(false)})}var l,s={},v=false,G={lang:"en",iconsize:"m",bkgd_color:"#FFF",bkgd_url:"",img_save:"embed"}, -e={},f={canvasName:"default",canvas_expansion:3,dimensions:[640,480],initFill:{color:"FF0000",opacity:1},initStroke:{width:5,color:"000000",opacity:1},text:{stroke_width:0,font_size:24,font_family:"serif"},initOpacity:1,imgPath:"images/",langPath:"locale/",extPath:"extensions/",jGraduatePath:"jgraduate/images/",extensions:["ext-markers.js","ext-connector.js","ext-eyedropper.js","ext-shapes.js","ext-imagelib.js","ext-grid.js"],initTool:"select",wireframe:false,colorPickerCSS:null,gridSnapping:false, -gridColor:"#000",baseUnit:"px",snappingStep:10,showRulers:true},k=s.uiStrings={common:{ok:"OK",cancel:"Cancel",key_up:"Up",key_down:"Down",key_backspace:"Backspace",key_del:"Del"},layers:{layer:"Layer"},notification:{invalidAttrValGiven:"Invalid value given",noContentToFitTo:"No content to fit to",dupeLayerName:"There is already a layer named that!",enterUniqueLayerName:"Please enter a unique layer name",enterNewLayerName:"Please enter the new layer name",layerHasThatName:"Layer already has that name", -QmoveElemsToLayer:'Move selected elements to layer "%s"?',QwantToClear:"Do you want to clear the drawing?\nThis will also erase your undo history!",QwantToOpen:"Do you want to open a new file?\nThis will also erase your undo history!",QerrorsRevertToSource:"There were parsing errors in your SVG source.\nRevert back to original SVG source?",QignoreSourceChanges:"Ignore changes made to SVG source?",featNotSupported:"Feature not supported",enterNewImgURL:"Enter the new image URL",defsFailOnSave:"NOTE: Due to a bug in your browser, this image may appear wrong (missing gradients or elements). It will however appear correct once actually saved.", -loadingImage:"Loading image, please wait...",saveFromBrowser:'Select "Save As..." in your browser to save this image as a %s file.',noteTheseIssues:"Also note the following issues: ",unsavedChanges:"There are unsaved changes.",enterNewLinkURL:"Enter the new hyperlink URL",errorLoadingSVG:"Error: Unable to load SVG data",URLloadFail:"Unable to load from URL",retrieving:'Retrieving "%s" ...'}};e={};var n={};s.curConfig=f;s.tool_scale=1;a.pref=function(B,A){if(A)e[B]=A;B="svg-edit-"+B;var O=location.hostname, -Z=O&&O.indexOf(".")>=0,N=A!=undefined,L=false;try{if(window.localStorage)L=localStorage}catch(qa){}try{if(window.globalStorage&&Z)L=globalStorage[O]}catch(aa){}if(L)if(N)L.setItem(B,A);else{if(L.getItem(B))return L.getItem(B)+""}else if(window.widget)if(N)widget.setPreferenceForKey(A,B);else return widget.preferenceForKey(B);else if(N){O=new Date;O.setTime(O.getTime()+31536E6);A=encodeURIComponent(A);document.cookie=B+"="+A+"; expires="+O.toUTCString()}else return(O=document.cookie.match(RegExp(B+ -"=([^;]+)")))?decodeURIComponent(O[1]):""};s.setConfig=function(B){a.each(B,function(A,O){A in G&&a.pref(A,O)});a.extend(true,f,B);if(B.extensions)f.extensions=B.extensions};s.setCustomHandlers=function(B){s.ready(function(){if(B.open){a('#tool_open > input[type="file"]').remove();a("#tool_open").show();l.open=B.open}if(B.save){s.show_save_warning=false;l.bind("saved",B.save)}B.pngsave&&l.bind("exported",B.pngsave);n=B})};s.randomizeIds=function(){l.randomizeIds(arguments)};s.init=function(){function B(g, -p){var D=g.id,K=D.split("_"),M=K[0];K=K[1];p&&l.setStrokeAttr("stroke-"+M,K);Oa();C("#cur_"+M,D,20);a(g).addClass("current").siblings().removeClass("current")}function A(g,p){a.pref("bkgd_color",g);a.pref("bkgd_url",p);l.setBackground(g,p)}function O(){var g=l.getHref(ha);g=g.indexOf("data:")===0?"":g;a.prompt(k.notification.enterNewImgURL,g,function(p){p&&ub(p)})}function Z(){if(l.deleteCurrentLayer()){ra();Xa();a("#layerlist tr.layer").removeClass("layersel");a("#layerlist tr.layer:first").addClass("layersel")}} -function N(){var g=l.getCurrentDrawing().getCurrentLayerName()+" copy";a.prompt(k.notification.enterUniqueLayerName,g,function(p){if(p)if(l.getCurrentDrawing().hasLayer(p))a.alert(k.notification.dupeLayerName);else{l.cloneLayer(p);ra();Xa()}})}function L(g){var p=a("#layerlist tr.layersel").index(),D=l.getCurrentDrawing().getNumLayers();if(p>0||p<D-1){p+=g;l.setCurrentLayerPosition(D-p-1);Xa()}}function qa(g,p){p||(p=l.getZoom());g||(g=a("#svgcanvas"));for(var D=l.getContentElem(),K=svgedit.units.getTypeMap()[f.baseUnit], -M=0;M<2;M++){var Q=M===0,U=Q?"x":"y",X=Q?"width":"height",ga=D.getAttribute(U)-0;U=a("#ruler_"+U+" canvas:first");$hcanv=U.clone();U.replaceWith($hcanv);var ea=$hcanv[0];var ma=U=g[X]();ea.parentNode.style[X]=ma+"px";var za=0,Ea,S=ea.getContext("2d");S.fillStyle="rgb(200,0,0)";S.fillRect(0,0,ea.width,ea.height);$hcanv.siblings().remove();if(U>=3E4){var Ba=parseInt(U/3E4)+1;Ea=Array(Ba);Ea[0]=S;for(var fa=1;fa<Ba;fa++){ea[X]=3E4;var ua=ea.cloneNode(true);ea.parentNode.appendChild(ua);Ea[fa]=ua.getContext("2d")}ua[X]= -U%3E4;U=3E4}ea[X]=U;X=K*p;var Fa=50/X;ea=1;for(fa=0;fa<Xb.length;fa++){ea=Ba=Xb[fa];if(Fa<=Ba)break}Fa=ea*X;S.font="9px sans-serif";for(var Ja=ga/X%ea*X,Na=Ja-Fa;Ja<ma;Ja+=Fa){Na+=Fa;fa=Math.round(Ja)+0.5;if(Q){S.moveTo(fa,15);S.lineTo(fa,0)}else{S.moveTo(15,fa);S.lineTo(0,fa)}Ba=(Na-ga)/X;if(ea>=1)fa=Math.round(Ba);else{fa=(ea+"").split(".")[1].length;fa=Ba.toFixed(fa)-0}if(fa!==0&&fa!==1E3&&fa%1E3===0)fa=fa/1E3+"K";if(Q)S.fillText(fa,Ja+2,8);else{Ba=(fa+"").split("");for(fa=0;fa<Ba.length;fa++)S.fillText(Ba[fa], -1,Ja+9+fa*9)}Ba=Fa/10;for(fa=1;fa<10;fa++){var Ca=Math.round(Ja+Ba*fa)+0.5;if(Ea&&Ca>U){za++;S.stroke();if(za>=Ea.length){fa=10;Ja=ma;continue}S=Ea[za];Ja-=3E4;Ca=Math.round(Ja+Ba*fa)+0.5}var Ya=fa%2?12:10;if(Q){S.moveTo(Ca,15);S.lineTo(Ca,Ya)}else{S.moveTo(15,Ca);S.lineTo(Ya,Ca)}}}S.strokeStyle="#000";S.stroke()}}(function(){var g=window.opener;if(g)try{var p=g.document.createEvent("Event");p.initEvent("svgEditorReady",true,true);g.document.documentElement.dispatchEvent(p)}catch(D){}})();(function(){var g= -a.deparam.querystring(true);if(a.isEmptyObject(g))(g=window.localStorage.getItem("svgedit-"+s.curConfig.canvasName))&&s.loadFromString(g);else{if(g.dimensions)g.dimensions=g.dimensions.split(",");if(g.extensions)g.extensions=g.extensions.split(",");if(g.bkgd_color)g.bkgd_color="#"+g.bkgd_color;svgEditor.setConfig(g);var p=g.source,D=a.param.querystring();if(!p)if(D.indexOf("source=data:")>=0)p=D.match(/source=(data:[^&]*)/)[1];if(p)if(p.indexOf("data:")===0){p=p.replace(/ /g,"+");s.loadFromDataURI(p)}else s.loadFromString(p); -else if(D.indexOf("paramurl=")!==-1)svgEditor.loadFromURL(D.substr(9));else g.url&&svgEditor.loadFromURL(g.url)}})();var aa=function(){a.each(f.extensions,function(){var p=this;a.getScript(f.extPath+p,function(D){if(!D){D=document.createElement("script");D.src=f.extPath+p;document.querySelector("head").appendChild(D)}})});var g=[];a("#lang_select option").each(function(){g.push(this.value)});s.putLocale(null,g)};document.location.protocol==="file:"?setTimeout(aa,100):aa();a.svgIcons(f.imgPath+"svg_edit_icons.svg", -{w:24,h:24,id_match:false,no_img:!svgedit.browser.isWebkit(),fallback_path:f.imgPath,fallback:{new_image:"clear.png",save:"save.png",open:"open.png",source:"source.png",docprops:"document-properties.png",wireframe:"wireframe.png",undo:"undo.png",redo:"redo.png",select:"select.png",select_node:"select_node.png",pencil:"fhpath.png",pen:"line.png",square:"square.png",rect:"rect.png",fh_rect:"freehand-square.png",circle:"circle.png",ellipse:"ellipse.png",fh_ellipse:"freehand-circle.png",path:"path.png", -text:"text.png",image:"image.png",zoom:"zoom.png",clone:"clone.png",node_clone:"node_clone.png","delete":"delete.png",node_delete:"node_delete.png",group:"shape_group.png",ungroup:"shape_ungroup.png",move_top:"move_top.png",move_bottom:"move_bottom.png",to_path:"to_path.png",link_controls:"link_controls.png",reorient:"reorient.png",align_left:"align-left.png",align_center:"align-center",align_right:"align-right",align_top:"align-top",align_middle:"align-middle",align_bottom:"align-bottom",go_up:"go-up.png", -go_down:"go-down.png",ok:"save.png",cancel:"cancel.png",arrow_right:"flyouth.png",arrow_down:"dropdown.gif"},placement:{"#logo":"logo","#tool_clear div,#layer_new":"new_image","#tool_save div":"save","#tool_export div":"export","#tool_open div div":"open","#tool_import div div":"import","#tool_source":"source","#tool_docprops > div":"docprops","#tool_wireframe":"wireframe","#tool_undo":"undo","#tool_redo":"redo","#tool_select":"select","#tool_fhpath":"pencil","#tool_line":"pen","#tool_rect,#tools_rect_show":"rect", -"#tool_square":"square","#tool_fhrect":"fh_rect","#tool_ellipse,#tools_ellipse_show":"ellipse","#tool_circle":"circle","#tool_fhellipse":"fh_ellipse","#tool_path":"path","#tool_text,#layer_rename":"text","#tool_image":"image","#tool_zoom":"zoom","#tool_clone,#tool_clone_multi":"clone","#tool_node_clone":"node_clone","#layer_delete,#tool_delete,#tool_delete_multi":"delete","#tool_node_delete":"node_delete","#tool_add_subpath":"add_subpath","#tool_openclose_path":"open_path","#tool_move_top":"move_top", -"#tool_move_bottom":"move_bottom","#tool_topath":"to_path","#tool_node_link":"link_controls","#tool_reorient":"reorient","#tool_group":"group","#tool_ungroup":"ungroup","#tool_unlink_use":"unlink_use","#tool_alignleft, #tool_posleft":"align_left","#tool_aligncenter, #tool_poscenter":"align_center","#tool_alignright, #tool_posright":"align_right","#tool_aligntop, #tool_postop":"align_top","#tool_alignmiddle, #tool_posmiddle":"align_middle","#tool_alignbottom, #tool_posbottom":"align_bottom","#cur_position":"align", -"#linecap_butt,#cur_linecap":"linecap_butt","#linecap_round":"linecap_round","#linecap_square":"linecap_square","#linejoin_miter,#cur_linejoin":"linejoin_miter","#linejoin_round":"linejoin_round","#linejoin_bevel":"linejoin_bevel","#url_notice":"warning","#layer_up":"go_up","#layer_down":"go_down","#layer_moreopts":"context_menu","#layerlist td.layervis":"eye","#tool_source_save,#tool_docprops_save,#tool_prefs_save":"ok","#tool_source_cancel,#tool_docprops_cancel,#tool_prefs_cancel":"cancel","#rwidthLabel, #iwidthLabel":"width", -"#rheightLabel, #iheightLabel":"height","#cornerRadiusLabel span":"c_radius","#angleLabel":"angle","#linkLabel,#tool_make_link,#tool_make_link_multi":"globe_link","#zoomLabel":"zoom","#tool_fill label":"fill","#tool_stroke .icon_label":"stroke","#group_opacityLabel":"opacity","#blurLabel":"blur","#font_sizeLabel":"fontsize",".flyout_arrow_horiz":"arrow_right",".dropdown button, #main_button .dropdown":"arrow_down","#palette .palette_item:first, #fill_bg, #stroke_bg":"no_color"},resize:{"#logo .svg_icon":28, -".flyout_arrow_horiz .svg_icon":5,".layer_button .svg_icon, #layerlist td.layervis .svg_icon":14,".dropdown button .svg_icon":7,"#main_button .dropdown .svg_icon":9,".palette_item:first .svg_icon":15,"#fill_bg .svg_icon, #stroke_bg .svg_icon":16,".toolbar_button button .svg_icon":16,".stroke_tool div div .svg_icon":20,"#tools_bottom label .svg_icon":18},callback:function(){a(".toolbar_button button > svg, .toolbar_button button > img").each(function(){a(this).parent().prepend(this)});var g=a("#tools_left"); -if(g.length!=0){g.offset();g.outerHeight()}a(".tools_flyout").each(function(){var p=a("#"+this.id+"_show"),D=p.attr("data-curopt");if(!p.children("svg, img").length){D=a(D).children().clone();if(D.length){D[0].removeAttribute("style");p.append(D)}}});svgEditor.runCallbacks();setTimeout(function(){a(".flyout_arrow_horiz:empty").each(function(){a(this).append(a.getSvgIcon("arrow_right").width(5).height(5))})},1)}});s.canvas=l=new a.SvgCanvas(document.getElementById("svgcanvas"),f);s.show_save_warning= -false;aa=["#"+f.initFill.color,"#000000","#3f3f3f","#7f7f7f","#bfbfbf","#ffffff","#ff0000","#ff7f00","#ffff00","#7fff00","#00ff00","#00ff7f","#00ffff","#007fff","#0000ff","#7f00ff","#ff00ff","#ff007f","#7f0000","#7f3f00","#7f7f00","#3f7f00","#007f00","#007f3f","#007f7f","#003f7f","#00007f","#3f007f","#7f007f","#7f003f","#ffaaaa","#ffd4aa","#ffffaa","#d4ffaa","#aaffaa","#aaffd4","#aaffff","#aad4ff","#aaaaff","#d4aaff","#ffaaff","#ffaad4"];var ja=navigator.platform.indexOf("Mac")>=0,da=navigator.userAgent.indexOf("AppleWebKit")>= -0,na=ja?"meta+":"ctrl+",ka=l.pathActions,Ra=l.undoMgr,$b=svgedit.utilities,jb=f.imgPath+"logo.png",ba=a("#workarea"),gb=a("#cmenu_canvas");a("#cmenu_layers");var hb=null,ac=1,Va="crosshair",ya="crosshair",Da="toolbars",Ob="",Ka={fill:null,stroke:null};(function(){a("#dialog_container").draggable({cancel:"#dialog_content, #dialog_buttons *",containment:"window"});var g=a("#dialog_box"),p=a("#dialog_buttons"),D=function(K,M,Q,U){a("#dialog_content").html("<p>"+M.replace(/\n/g,"</p><p>")+"</p>").toggleClass("prompt", -K=="prompt");p.empty();var X=a('<input type="button" value="'+k.common.ok+'">').appendTo(p);K!="alert"&&a('<input type="button" value="'+k.common.cancel+'">').appendTo(p).click(function(){g.hide();Q(false)});if(K=="prompt"){var ga=a('<input type="text">').prependTo(p);ga.val(U||"");ga.bind("keydown","return",function(){X.click()})}K=="process"&&X.hide();g.show();X.click(function(){g.hide();var ea=K=="prompt"?ga.val():true;Q&&Q(ea)}).focus();K=="prompt"&&ga.focus()};a.alert=function(K,M){D("alert", -K,M)};a.confirm=function(K,M){D("confirm",K,M)};a.process_cancel=function(K,M){D("process",K,M)};a.prompt=function(K,M,Q){D("prompt",K,Q,M)}})();var sb=function(){var g=a(".tool_button_current");if(g.length&&g[0].id!=="tool_select"){g.removeClass("tool_button_current").addClass("tool_button");a("#tool_select").addClass("tool_button_current").removeClass("tool_button");a("#styleoverrides").text("#svgcanvas svg *{cursor:move;pointer-events:all} #svgcanvas svg{cursor:default}")}l.setMode("select");ba.css("cursor", -"auto")},ha=null,xa=false,La=false,Ha=false,ta=false,oa="",wa=a("title:first").text(),pa=l.zoomChanged=function(g,p,D){l.getResolution();a("#svgcanvas").position();if(p=l.setBBoxZoom(p,ba.width()-15,ba.height()-15)){g=p.zoom;p=p.bbox;if(g<0.001)Za({value:0.1});else{a("#zoom").val(g*100);D?qb():qb(false,{x:p.x*g+p.width*g/2,y:p.y*g+p.height*g/2});l.getMode()=="zoom"&&p.width&&sb();m()}}};a("#cur_context_panel").delegate("a","click",function(){var g=a(this);g.attr("data-root")?l.leaveContext():l.setContext(g.text()); -l.clearSelection();return false});var mb={},Pb=function(g){a.each(g,function(p,D){var K=a(p).children(),M=p+"_show",Q=a(M),U=false;K.addClass("tool_button").unbind("click mousedown mouseup").each(function(ea){var ma=D[ea];mb[ma.sel]=ma.fn;if(ma.isDefault)U=ea;ea=function(za){var Ea=ma;if(za.type==="keydown"){var S=a(Ea.parent+"_show").hasClass("tool_button_current"),Ba=a(Ea.parent+"_show").attr("data-curopt");a.each(g[ma.parent],function(ua,Fa){if(Fa.sel==Ba)Ea=!za.shiftKey||!S?Fa:g[ma.parent][ua+ -1]||g[ma.parent][0]})}if(a(this).hasClass("disabled"))return false;la(M)&&Ea.fn();var fa=Ea.icon?a.getSvgIcon(Ea.icon,true):a(Ea.sel).children().eq(0).clone();fa[0].setAttribute("width",Q.width());fa[0].setAttribute("height",Q.height());Q.children(":not(.flyout_arrow_horiz)").remove();Q.append(fa).attr("data-curopt",Ea.sel)};a(this).mouseup(ea);ma.key&&a(document).bind("keydown",ma.key[0]+" shift+"+ma.key[0],ea)});if(U)Q.attr("data-curopt",D[U].sel);else Q.attr("data-curopt")||Q.attr("data-curopt", -D[0].sel);var X,ga=a(M).position();a(p).css({left:ga.left+34,top:ga.top+40});Q.mousedown(function(ea){if(Q.hasClass("disabled"))return false;var ma=a(p),za=ga.left+34,Ea=ma.width()*-1,S=ma.data("shown_popop")?200:0;X=setTimeout(function(){Q.data("isLibrary")?ma.css("left",za).show():ma.css("left",Ea).show().animate({left:za},150);ma.data("shown_popop",true)},S);ea.preventDefault()}).mouseup(function(){clearTimeout(X);var ea=a(this).attr("data-curopt");if(Q.data("isLibrary")&&a(M.replace("_show","")).is(":visible"))la(M, -true);else la(M)&&ea in mb&&mb[ea]()})});Db()},tb=function(g,p){return a("<div>",{"class":"tools_flyout",id:g}).appendTo("#svg_editor").append(p)},Fb=function(){a(".tools_flyout").each(function(){var g=a("#"+this.id+"_show"),p=g.offset();g=g.outerWidth();a(this).css({left:(p.left+g)*ac,top:p.top})})},Db=function(){a(".tools_flyout").each(function(){var g=a("#"+this.id+"_show");if(!g.data("isLibrary")){var p=[];a(this).children().each(function(){p.push(this.title)});g[0].title=p.join(" / ")}})},nb, -Rb=function(g,p,D){var K=null;if(g.indexOf("url(#")===0){g=(g=l.getRefElem(g))?g.cloneNode(true):a("#"+D+"_color defs *")[0];K={alpha:p};K[g.tagName]=g}else K=g.indexOf("#")===0?{alpha:p,solidColor:g.substr(1)}:{alpha:p,solidColor:"none"};return new a.jGraduate.Paint(K)},ub=s.setImageURL=function(g){g||(g=jb);l.setImageURL(g);a("#image_url").val(g);if(g.indexOf("data:")===0){a("#image_url").hide();a("#change_image_url").show()}else{l.embedImage(g,function(p){p?a("#url_notice").hide():a("#url_notice").show(); -jb=g});a("#image_url").show();a("#change_image_url").hide()}},lb=function(g){var p=Math.min(Math.max(12+g.value.length*6,50),300);a(g).width(p)},ra=function(){var g=ha;if(g!=null&&!g.parentNode)g=null;var p=l.getCurrentDrawing().getCurrentLayerName(),D=l.getMode(),K=f.baseUnit!=="px"?f.baseUnit:null,M=D=="pathedit",Q=a("#cmenu_canvas li");a("#selected_panel, #multiselected_panel, #g_panel, #rect_panel, #circle_panel,\t\t\t\t\t#ellipse_panel, #line_panel, #text_panel, #image_panel, #container_panel, #use_panel, #a_panel").hide(); -if(g!=null){var U=g.nodeName,X=l.getRotationAngle(g);a("#angle").val(X);var ga=l.getBlur(g);a("#blur").val(ga);a("#blur_slider").slider("option","value",ga);l.addedNew&&U==="image"&&l.getHref(g).indexOf("data:")!==0&&O();if(!M&&D!="pathedit"){a("#selected_panel").show();if(["line","circle","ellipse"].indexOf(U)>=0)a("#xy_panel").hide();else{var ea,ma;if(["g","polyline","path"].indexOf(U)>=0){if(D=l.getStrokedBBox([g])){ea=D.x;ma=D.y}}else{ea=g.getAttribute("x");ma=g.getAttribute("y")}if(K){ea=svgedit.units.convertUnit(ea); -ma=svgedit.units.convertUnit(ma)}a("#selected_x").val(ea||0);a("#selected_y").val(ma||0);a("#xy_panel").show()}K=["image","text","path","g","use"].indexOf(U)==-1;a("#tool_topath").toggle(K);a("#tool_reorient").toggle(U=="path");a("#tool_reorient").toggleClass("disabled",X==0)}else{p=ka.getNodePoint();a("#tool_add_subpath").removeClass("push_button_pressed").addClass("tool_button");a("#tool_node_delete").toggleClass("disabled",!ka.canDeleteNodes);C("#tool_openclose_path",ka.closed_subpath?"open_path": -"close_path");if(p){M=a("#seg_type");if(K){p.x=svgedit.units.convertUnit(p.x);p.y=svgedit.units.convertUnit(p.y)}a("#path_node_x").val(p.x);a("#path_node_y").val(p.y);p.type?M.val(p.type).removeAttr("disabled"):M.val(4).attr("disabled","disabled")}return}K={g:[],a:[],rect:["rx","width","height"],image:["width","height"],circle:["cx","cy","r"],ellipse:["cx","cy","rx","ry"],line:["x1","y1","x2","y2"],text:[],use:[]};var za=g.tagName;U=null;if(za==="a"){U=l.getHref(g);a("#g_panel").show()}if(g.parentNode.tagName=== -"a")if(!a(g).siblings().length){a("#a_panel").show();U=l.getHref(g.parentNode)}a("#tool_make_link, #tool_make_link").toggle(!U);U&&a("#link_url").val(U);if(K[za]){K=K[za];a("#"+za+"_panel").show();a.each(K,function(Ea,S){var Ba=g.getAttribute(S);if(f.baseUnit!=="px"&&g[S])Ba=svgedit.units.convertUnit(g[S].baseVal.value);a("#"+za+"_"+S).val(Ba||0)});if(za=="text"){a("#text_panel").css("display","inline");l.getItalic()?a("#tool_italic").addClass("push_button_pressed").removeClass("tool_button"):a("#tool_italic").removeClass("push_button_pressed").addClass("tool_button"); -l.getBold()?a("#tool_bold").addClass("push_button_pressed").removeClass("tool_button"):a("#tool_bold").removeClass("push_button_pressed").addClass("tool_button");a("#font_family").val(g.getAttribute("font-family"));a("#font_size").val(g.getAttribute("font-size"));a("#text").val(g.textContent);l.addedNew&&setTimeout(function(){a("#text").focus().select()},100)}else if(za=="image")ub(l.getHref(g));else if(za==="g"||za==="use"){a("#container_panel").show();K=l.getTitle();U=a("#g_title")[0];U.value=K; -lb(U);za=="use"?U.setAttribute("disabled","disabled"):U.removeAttribute("disabled")}}Q[(za==="g"?"en":"dis")+"ableContextMenuItems"]("#ungroup");Q[(za==="g"||!xa?"dis":"en")+"ableContextMenuItems"]("#group")}else if(xa){a("#multiselected_panel").show();Q.enableContextMenuItems("#group").disableContextMenuItems("#ungroup")}else Q.disableContextMenuItems("#delete,#cut,#copy,#group,#ungroup,#move_front,#move_up,#move_down,#move_back");Ra.getUndoStackSize()>0?a("#tool_undo").removeClass("disabled"):a("#tool_undo").addClass("disabled"); -Ra.getRedoStackSize()>0?a("#tool_redo").removeClass("disabled"):a("#tool_redo").addClass("disabled");l.addedNew=false;if(g&&!M||xa){a("#selLayerNames").removeAttr("disabled").val(p);gb.enableContextMenuItems("#delete,#cut,#copy,#move_front,#move_up,#move_down,#move_back")}else a("#selLayerNames").attr("disabled","disabled")};a("#text").focus(function(){});a("#text").blur(function(){});l.bind("selected",function(g,p){var D=l.getMode();D==="select"&&sb();D=D=="pathedit";ha=p.length==1||p[1]==null?p[0]: -null;xa=p.length>=2&&p[1]!=null;if(ha!=null)if(!D){if(ha!=null)switch(ha.tagName){case "use":case "image":case "foreignObject":break;case "g":case "a":for(var K=null,M=ha.getElementsByTagName("*"),Q=0,U=M.length;Q<U;Q++){var X=M[Q].getAttribute("stroke-width");if(Q===0)K=X;else if(K!==X)K=null}a("#stroke_width").val(K===null?"":K);Ka.fill.update(true);Ka.stroke.update(true);break;default:Ka.fill.update(true);Ka.stroke.update(true);a("#stroke_width").val(ha.getAttribute("stroke-width")||1);a("#stroke_style").val(ha.getAttribute("stroke-dasharray")|| -"none");K=ha.getAttribute("stroke-linejoin")||"miter";a("#linejoin_"+K).length!=0&&B(a("#linejoin_"+K)[0]);K=ha.getAttribute("stroke-linecap")||"butt";a("#linecap_"+K).length!=0&&B(a("#linecap_"+K)[0])}if(ha!=null){K=(ha.getAttribute("opacity")||1)*100;a("#group_opacity").val(K);a("#opac_slider").slider("option","value",K);a("#elem_id").val(ha.id)}bb()}a("#path_node_panel").toggle(D);a("#tools_bottom_2,#tools_bottom_3").toggle(!D);if(D){a(".tool_button_current").removeClass("tool_button_current").addClass("tool_button"); -a("#tool_select").addClass("tool_button_current").removeClass("tool_button");C("#tool_select","select_node");xa=false;if(p.length)ha=p[0]}else C("#tool_select","select");ra();l.runExtensions("selectedChanged",{elems:p,selectedElement:ha,multiselected:xa})});l.bind("transition",function(g,p){var D=l.getMode(),K=p[0];if(K){xa=p.length>=2&&p[1]!=null;if(!xa)switch(D){case "rotate":D=l.getRotationAngle(K);a("#angle").val(D);a("#tool_reorient").toggleClass("disabled",D==0)}l.runExtensions("elementTransition", -{elems:p})}});l.bind("changed",function(g,p){var D=l.getMode();D==="select"&&sb();for(var K=0;K<p.length;++K){var M=p[K];if(M&&M.tagName==="svg"){Xa();qb()}else if(M&&ha&&ha.parentNode==null)ha=M}s.show_save_warning=true;ra();if(ha&&D==="select"){Ka.fill.update();Ka.stroke.update()}l.runExtensions("elementChanged",{elems:p})});l.bind("saved",function(g,p){s.show_save_warning=false;p='<?xml version="1.0"?>\n'+p;var D=navigator.userAgent;if(~D.indexOf("Chrome")&&a.browser.version>=533||~D.indexOf("MSIE"))i(0, -true);else{var K=g.open("data:image/svg+xml;base64,"+$b.encode64(p)),M=a.pref("save_notice_done");if(M!=="all"){var Q=k.notification.saveFromBrowser.replace("%s","SVG");if(D.indexOf("Gecko/")!==-1)if(p.indexOf("<defs")!==-1){Q+="\n\n"+k.notification.defsFailOnSave;a.pref("save_notice_done","all");M="all"}else a.pref("save_notice_done","part");else a.pref("save_notice_done","all");M!=="part"&&K.alert(Q)}}});l.bind("exported",function(g,p){var D=p.issues;a("#export_canvas").length||a("<canvas>",{id:"export_canvas"}).hide().appendTo("body"); -var K=a("#export_canvas")[0];K.width=l.contentW;K.height=l.contentH;canvg(K,p.svg,{renderCallback:function(){var M=K.toDataURL("image/png");hb.location.href=M;if(a.pref("export_notice_done")!=="all"){M=k.notification.saveFromBrowser.replace("%s","PNG");if(D.length)M+="\n\n"+k.notification.noteTheseIssues+"\n \u2022 "+D.join("\n \u2022 ");a.pref("export_notice_done","all");hb.alert(M)}}})});l.bind("zoomed",pa);l.bind("contextset",function(g,p){var D="";if(p){var K="";D='<a href="#" data-root="y">'+ -l.getCurrentDrawing().getCurrentLayerName()+"</a>";a(p).parentsUntil("#svgcontent > g").andSelf().each(function(){if(this.id){K+=" > "+this.id;D+=this!==p?' > <a href="#">'+this.id+"</a>":" > "+this.id}});oa=K}else oa=null;a("#cur_context_panel").toggle(!!p).html(D);w()});l.bind("extension_added",function(g,p){function D(){if(nb){clearTimeout(nb);nb=null}M||(nb=setTimeout(function(){M=true;I(e.iconsize)},50))}var K=false,M=false,Q=true,U=function(){if(p.callback&&!K&&Q){K=true;p.callback()}},X=[]; -p.context_tools&&a.each(p.context_tools,function(Ea,S){var Ba=S.container_id?' id="'+S.container_id+'"':"",fa=a("#"+S.panel);fa.length||(fa=a("<div>",{id:S.panel}).appendTo("#tools_top"));switch(S.type){case "tool_button":var ua='<div class="tool_button">'+S.id+"</div>",Fa=a(ua).appendTo(fa);S.events&&a.each(S.events,function(Ca,Ya){a(Fa).bind(Ca,Ya)});break;case "select":ua="<label"+Ba+'><select id="'+S.id+'">';a.each(S.options,function(Ca,Ya){ua+='<option value="'+Ca+'"'+(Ca==S.defval?" selected": -"")+">"+Ya+"</option>"});ua+="</select></label>";var Ja=a(ua).appendTo(fa).find("select");a.each(S.events,function(Ca,Ya){a(Ja).bind(Ca,Ya)});break;case "button-select":ua='<div id="'+S.id+'" class="dropdown toolset" title="'+S.title+'"><div id="cur_'+S.id+'" class="icon_label"></div><button></button></div>';Ba=a('<ul id="'+S.id+'_opts"></ul>').appendTo("#option_lists");S.colnum&&Ba.addClass("optcols"+S.colnum);a(ua).appendTo(fa).children();X.push({elem:"#"+S.id,list:"#"+S.id+"_opts",title:S.title, -callback:S.events.change,cur:"#cur_"+S.id});break;case "input":ua="<label"+Ba+'><span id="'+S.id+'_label">'+S.label+':</span><input id="'+S.id+'" title="'+S.title+'" size="'+(S.size||"4")+'" value="'+(S.defval||"")+'" type="text"/></label>';var Na=a(ua).appendTo(fa).find("input");S.spindata&&Na.SpinButton(S.spindata);S.events&&a.each(S.events,function(Ca,Ya){Na.bind(Ca,Ya)})}});if(p.buttons){var ga={},ea={},ma=p.svgicons,za={};a.each(p.buttons,function(Ea,S){for(var Ba,fa=S.id,ua=Ea;a("#"+fa).length;)fa= -S.id+"_"+ ++ua;if(ma){ga[fa]=S.icon;ua=S.svgicon?S.svgicon:S.id;if(S.type=="app_menu")ea["#"+fa+" > div"]=ua;else ea["#"+fa]=ua}else Ba=a('<img src="'+S.icon+'">');var Fa,Ja;switch(S.type){case "mode_flyout":case "mode":Fa="tool_button";Ja="#tools_left";break;case "context":Fa="tool_button";Ja="#"+S.panel;a(Ja).length||a("<div>",{id:S.panel}).appendTo("#tools_top");break;case "app_menu":Fa="";Ja="#main_menu ul"}var Na=a(S.list||S.type=="app_menu"?"<li/>":"<div/>").attr("id",fa).attr("title",S.title).addClass(Fa); -if(!S.includeWith&&!S.list){"position"in S?a(Ja).children().eq(S.position).before(Na):Na.appendTo(Ja);if(S.type=="mode_flyout"){ua=a(Na);Fa=ua.parent();if(!ua.parent().hasClass("tools_flyout")){var Ca=ua[0].id.replace("tool_","tools_"),Ya=ua.clone().attr("id",Ca+"_show").append(a("<div>",{"class":"flyout_arrow_horiz"}));ua.before(Ya);Fa=tb(Ca,ua);Fa.data("isLibrary",true);Ya.data("isLibrary",true)}ea["#"+Ca+"_show"]=S.id;fa=za["#"+Fa[0].id]=[{sel:"#"+fa,fn:S.events.click,icon:S.id,isDefault:true}, -ic]}else S.type=="app_menu"&&Na.append("<div>").append(S.title)}else if(S.list){Na.addClass("push_button");a("#"+S.list+"_opts").append(Na);if(S.isDefault){a("#cur_"+S.list).append(Na.children().clone());ua=S.svgicon?S.svgicon:S.id;ea["#cur_"+S.list]=ua}}else if(S.includeWith){Ja=S.includeWith;ua=a(Ja.button);Fa=ua.parent();if(!ua.parent().hasClass("tools_flyout")){Ca=ua[0].id.replace("tool_","tools_");Ya=ua.clone().attr("id",Ca+"_show").append(a("<div>",{"class":"flyout_arrow_horiz"}));ua.before(Ya); -Fa=tb(Ca,ua)}var ic=mc.getButtonData(Ja.button);if(Ja.isDefault)ea["#"+Ca+"_show"]=S.id;fa=za["#"+Fa[0].id]=[{sel:"#"+fa,fn:S.events.click,icon:S.id,key:S.key,isDefault:S.includeWith?S.includeWith.isDefault:0},ic];Ca="position"in Ja?Ja.position:"last";ic=Fa.children().length;if(!isNaN(Ca)&&Ca>=0&&Ca<ic)Fa.children().eq(Ca).before(Na);else{Fa.append(Na);fa.reverse()}}ma||Na.append(Ba);S.list||a.each(S.events,function(Yb,Zb){if(Yb=="click")if(S.type=="mode"){S.includeWith?Na.bind(Yb,Zb):Na.bind(Yb, -function(){la(Na)&&Zb()});if(S.key){a(document).bind("keydown",S.key,Zb);S.title&&Na.attr("title",S.title+" ["+S.key+"]")}}else Na.bind(Yb,Zb);else Na.bind(Yb,Zb)});Pb(za)});a.each(X,function(){fb(this.elem,this.list,this.callback,{seticon:true})});if(ma)Q=false;a.svgIcons(ma,{w:24,h:24,id_match:false,no_img:!da,fallback:ga,placement:ea,callback:function(){e.iconsize&&e.iconsize!="m"&&D();Q=true;U()}})}U()});l.textActions.setInputElem(a("#text")[0]);var Ma='<div class="palette_item" data-rgb="none"></div>'; -a.each(aa,function(g,p){Ma+='<div class="palette_item" style="background-color: '+p+';" data-rgb="'+p+'"></div>'});a("#palette").append(Ma);Ma="";a.each(["#FFF","#888","#000"],function(){Ma+='<div class="color_block" style="background-color:'+this+';"></div>'});a("#bg_blocks").append(Ma);var va=a("#bg_blocks div");va.each(function(){a(this).click(function(){va.removeClass("cur_background");a(this).addClass("cur_background")})});if(a.pref("bkgd_color"))A(a.pref("bkgd_color"),a.pref("bkgd_url"));else a.pref("bkgd_url")&& -A(G.bkgd_color,a.pref("bkgd_url"));if(a.pref("img_save")){e.img_save=a.pref("img_save");a("#image_save_opts input").val([e.img_save])}var Za=function(g){var p=g.value/100;if(p<0.001)g.value=0.1;else{g=l.getZoom();pa(window,{width:0,height:0,x:(ba[0].scrollLeft+ba.width()/2)/g,y:(ba[0].scrollTop+ba.height()/2)/g,zoom:p},true)}},ob=function(g,p){if(p==null)p=g.value;a("#group_opacity").val(p);if(!g||!g.handle)a("#opac_slider").slider("option","value",p);l.setOpacity(p/100)},Ua=function(g,p,D){if(p== -null)p=g.value;a("#blur").val(p);var K=false;if(!g||!g.handle){a("#blur_slider").slider("option","value",p);K=true}D?l.setBlurNoUndo(p):l.setBlur(p,K)},Oa=function(){window.opera&&a("<p/>").hide().appendTo("body").remove()};a("#stroke_style").change(function(){l.setStrokeAttr("stroke-dasharray",a(this).val());Oa()});a("#stroke_linejoin").change(function(){l.setStrokeAttr("stroke-linejoin",a(this).val());Oa()});a("select").change(function(){a(this).blur()});var Gb=false;a("#selLayerNames").change(function(){var g= -this.options[this.selectedIndex].value,p=k.notification.QmoveElemsToLayer.replace("%s",g),D=function(K){if(K){Gb=true;l.moveSelectedToLayer(g);l.clearSelection();Xa()}};if(g)Gb?D(true):a.confirm(p,D)});a("#font_family").change(function(){l.setFontFamily(this.value)});a("#seg_type").change(function(){l.setSegType(a(this).val())});a("#text").keyup(function(){l.setTextContent(this.value)});a("#image_url").change(function(){ub(this.value)});a("#link_url").change(function(){this.value.length?l.setLinkURL(this.value): -l.removeHyperlink()});a("#g_title").change(function(){l.setGroupTitle(this.value)});a(".attr_changer").change(function(){var g=this.getAttribute("data-attr"),p=this.value;if(!svgedit.units.isValidUnit(g,p,ha)){a.alert(k.notification.invalidAttrValGiven);this.value=ha.getAttribute(g);return false}if(g!=="id")if(isNaN(p))p=l.convertToNum(g,p);else if(f.baseUnit!=="px"){var D=svgedit.units.getTypeMap();if(ha[g]||l.getMode()==="pathedit"||g==="x"||g==="y")p*=D[f.baseUnit]}if(g==="id"){g=ha;l.clearSelection(); -g.id=p;l.addToSelection([g],true)}else l.changeSelectedAttribute(g,p);this.blur()});a("#palette").mouseover(function(){var g=a('<input type="hidden">');a(this).append(g);g.focus().remove()});a(".palette_item").mousedown(function(g){var p=g.button===2;p=(g=g.shiftKey||p)?"stroke":"fill";var D=a(this).attr("data-rgb"),K=null;if(D==="none"||D==="transparent"||D==="initial"){D="none";K=new a.jGraduate.Paint}else K=new a.jGraduate.Paint({alpha:100,solidColor:D.substr(1)});Ka[p].setPaint(K);if(g){l.setColor("stroke", -D);D!="none"&&l.getStrokeOpacity()!=1&&l.setPaintOpacity("stroke",1)}else{l.setColor("fill",D);D!="none"&&l.getFillOpacity()!=1&&l.setPaintOpacity("fill",1)}bb()}).bind("contextmenu",function(g){g.preventDefault()});a("#toggle_stroke_tools").on("click",function(){a("#tools_bottom").toggleClass("expanded")});var la=function(g,p){if(a(g).hasClass("disabled"))return false;if(a(g).parent().hasClass("tools_flyout"))return true;var D=D||"normal";p||a(".tools_flyout").fadeOut(D);a("#styleoverrides").text(""); -ba.css("cursor","auto");a(".tool_button_current").removeClass("tool_button_current").addClass("tool_button");a(g).addClass("tool_button_current").removeClass("tool_button");return true};(function(){var g=null,p=null,D=ba[0],K=false,M=false;a("#svgcanvas").bind("mousemove mouseup",function(Q){if(K!==false){D.scrollLeft-=Q.clientX-g;D.scrollTop-=Q.clientY-p;g=Q.clientX;p=Q.clientY;if(Q.type==="mouseup")K=false;return false}}).mousedown(function(Q){if(Q.button===1||M===true){K=true;g=Q.clientX;p=Q.clientY; -return false}});a(window).mouseup(function(){K=false});a(document).bind("keydown","space",function(Q){l.spaceKey=M=true;Q.preventDefault()}).bind("keyup","space",function(Q){Q.preventDefault();l.spaceKey=M=false}).bind("keydown","shift",function(){l.getMode()==="zoom"&&ba.css("cursor",ya)}).bind("keyup","shift",function(){l.getMode()==="zoom"&&ba.css("cursor",Va)})})();(function(){var g=a("#main_icon"),p=a("#main_icon span"),D=a("#main_menu"),K=false,M=0,Q=true,U=false;a(window).mouseup(function(ga){if(!K){g.removeClass("buttondown"); -if(ga.target.tagName!="INPUT")D.fadeOut(200);else if(!U){U=true;a(ga.target).click(function(){D.css("margin-left","-9999px").show()})}}K=false}).mousedown(function(ga){a(ga.target).closest("div.tools_flyout, .contextMenu").length||a(".tools_flyout:visible,.contextMenu").fadeOut(250)});p.bind("mousedown",function(){if(g.hasClass("buttondown")){g.removeClass("buttondown").addClass("buttonup");D.fadeOut(200)}else{g.addClass("buttondown").removeClass("buttonup");D.css("margin-left",0).show();M||(M=D.height()); -D.css("height",0).animate({height:M},200);K=true;return false}}).hover(function(){K=true}).mouseout(function(){K=false});var X=a("#main_menu li");X.mouseover(function(){Q=a(this).css("background-color")=="rgba(0, 0, 0, 0)";X.unbind("mouseover");Q&&X.mouseover(function(){this.style.backgroundColor="#FFC"}).mouseout(function(){this.style.backgroundColor="transparent";return true})})})();s.addDropDown=function(g,p,D){if(a(g).length!=0){var K=a(g).find("button"),M=a(g).find("ul").attr("id",a(g)[0].id+ -"-list");D||a("#option_lists").append(M);var Q=false;D&&a(g).addClass("dropup");M.find("li").bind("mouseup",p);a(window).mouseup(function(){if(!Q){K.removeClass("down");M.hide()}Q=false});K.bind("mousedown",function(){if(K.hasClass("down")){K.removeClass("down");M.hide()}else{K.addClass("down");if(!D){var U=a(g).position();M.css({top:U.top+24,left:U.left-10})}M.show();Q=true}}).hover(function(){Q=true}).mouseout(function(){Q=false})}};var fb=function(g,p,D,K){var M=a(g);p=a(p);var Q=false,U=K.dropUp; -U&&a(g).addClass("dropup");p.find("li").bind("mouseup",function(){if(K.seticon){C("#cur_"+M[0].id,a(this).children());a(this).addClass("current").siblings().removeClass("current")}D.apply(this,arguments)});a(window).mouseup(function(){if(!Q){M.removeClass("down");p.hide();p.css({top:0,left:0})}Q=false});p.height();a(g).bind("mousedown",function(){var X=a(g).offset();if(U){X.top-=p.height();X.left+=8}else X.top+=a(g).height();a(p).offset(X);if(M.hasClass("down")){M.removeClass("down");p.hide();p.css({top:0, -left:0})}else{M.addClass("down");p.show();Q=true;return false}}).hover(function(){Q=true}).mouseout(function(){Q=false});K.multiclick&&p.mousedown(function(){Q=true})};s.addDropDown("#font_family_dropdown",function(){a(this).text();a("#font_family").val(a(this).text()).change()});s.addDropDown("#opacity_dropdown",function(){if(!a(this).find("div").length){var g=parseInt(a(this).text().split("%")[0]);ob(false,g)}},true);a("#opac_slider").slider({start:function(){a("#opacity_dropdown li:not(.special)").hide()}, -stop:function(){a("#opacity_dropdown li").show();a(window).mouseup()},slide:function(g,p){ob(p)}});s.addDropDown("#blur_dropdown",a.noop);var zb=false;a("#blur_slider").slider({max:10,step:0.1,stop:function(g,p){zb=false;Ua(p);a("#blur_dropdown li").show();a(window).mouseup()},start:function(){zb=true},slide:function(g,p){Ua(p,null,zb)}});s.addDropDown("#zoom_dropdown",function(){var g=a(this),p=g.attr("data-val");p?pa(window,p):Za({value:parseInt(g.text())})},true);fb("#stroke_linecap","#linecap_opts", -function(){B(this,true)},{dropUp:true});fb("#stroke_linejoin","#linejoin_opts",function(){B(this,true)},{dropUp:true});fb("#tool_position","#position_opts",function(){var g=this.id.replace("tool_pos","").charAt(0);l.alignSelectedElements(g,"page")},{multiclick:true});(function(){var g,p=function(){a(g).blur()};a("#svg_editor").find("button, select, input:not(#text)").focus(function(){g=this;Da="toolbars";ba.mousedown(p)}).blur(function(){Da="canvas";ba.unbind("mousedown",p);l.getMode()=="textedit"&& -a("#text").focus()})})();var Hb=function(){if(la("#tool_select")){l.setMode("select");a("#styleoverrides").text("#svgcanvas svg *{cursor:move;pointer-events:all}, #svgcanvas svg{cursor:default}")}},Qb=function(){la("#tool_fhpath")&&l.setMode("fhpath")},Ab=function(){la("#tool_line")&&l.setMode("line")},Bb=function(){la("#tool_square")&&l.setMode("square")},Sb=function(){la("#tool_rect")&&l.setMode("rect")},Ib=function(){la("#tool_fhrect")&&l.setMode("fhrect")},kc=function(){la("#tool_circle")&&l.setMode("circle")}, -bc=function(){la("#tool_ellipse")&&l.setMode("ellipse")},vb=function(){la("#tool_fhellipse")&&l.setMode("fhellipse")},$a=function(){la("#tool_image")&&l.setMode("image")},Ga=function(){if(la("#tool_zoom")){l.setMode("zoom");ba.css("cursor",Va)}},ia=function(){if(la("#tool_zoom")){c();sb()}},rb=function(){la("#tool_text")&&l.setMode("text")},Tb=function(){la("#tool_path")&&l.setMode("path")},Kb=function(){if(ha!=null||xa)l.deleteSelectedElements()},Ub=function(){if(ha!=null||xa)l.cutSelectedElements()}, -cc=function(){if(ha!=null||xa)l.copySelectedElements()},Lb=function(){var g=l.getZoom(),p=(ba[0].scrollLeft+ba.width()/2)/g-l.contentW;g=(ba[0].scrollTop+ba.height()/2)/g-l.contentH;l.pasteElements("point",p,g)},dc=function(){ha!=null&&l.moveToTopSelectedElement()},Sa=function(){ha!=null&&l.moveToBottomSelectedElement()},cb=function(g){ha!=null&&l.moveUpDownSelected(g)},Aa=function(){ha!=null&&l.convertToPath()},db=function(){ha!=null&&ka.reorient()},ib=function(){if(ha!=null||xa)a.prompt(k.notification.enterNewLinkURL, -"http://",function(g){g&&l.makeHyperlink(g)})},eb=function(g,p){if(ha!=null||xa){if(f.gridSnapping){var D=l.getZoom()*f.snappingStep;g*=D;p*=D}l.moveSelectedElements(g,p)}},Jb=function(){var g=!a("#tool_node_link").hasClass("push_button_pressed");g?a("#tool_node_link").addClass("push_button_pressed").removeClass("tool_button"):a("#tool_node_link").removeClass("push_button_pressed").addClass("tool_button");ka.linkControlPoints(g)},Qa=function(){ka.getNodePoint()&&ka.clonePathNode()},sa=function(){ka.getNodePoint()&& -ka.deletePathNode()},fc=function(){var g=a("#tool_add_subpath"),p=!g.hasClass("push_button_pressed");p?g.addClass("push_button_pressed").removeClass("tool_button"):g.removeClass("push_button_pressed").addClass("tool_button");ka.addSubPath(p)},Wb=function(){ka.opencloseSubPath()},Eb=function(){l.cycleElement(1)},Nb=function(){l.cycleElement(0)},Cb=function(g,p){if(!(ha==null||xa)){g||(p*=-1);var D=a("#angle").val()*1+p;l.setRotationAngle(D);ra()}},yb=function(){var g=f.dimensions;a.confirm(k.notification.QwantToClear, -function(p){if(p){sb();l.clear();l.setResolution(g[0],g[1]);qb(true);c();Xa();ra();Ka.fill.prep();Ka.stroke.prep();l.runExtensions("onNewDocument")}})},Mb=function(){l.setBold(!l.getBold());ra();return false},lc=function(){l.setItalic(!l.getItalic());ra();return false},xb=function(){if(!n.pngsave){var g=k.notification.loadingImage;hb=window.open("data:text/html;charset=utf-8,<title>"+g+"

      "+g+"

      ")}window.canvg?l.rasterExport():a.getScript("canvg/rgbcolor.js",function(){a.getScript("canvg/canvg.js", -function(){l.rasterExport()})})},Ta=function(){l.open()},hc=function(){},gc=function(){if(Ra.getUndoStackSize()>0){Ra.undo();Xa()}},pb=function(){if(Ra.getRedoStackSize()>0){Ra.redo();Xa()}},Pa=function(){if(xa)l.groupSelectedElements();else ha&&l.ungroupSelectedElement()},Vb=function(){l.cloneSelectedElements(20,20)},b=function(){var g=this.id.replace("tool_align","").charAt(0);l.alignSelectedElements(g,a("#align_relative_to").val())},c=function(g){var p=l.getResolution();g=g?p.zoom*g:1;a("#zoom").val(g* -100);l.setZoom(g);m();qb(true)},d=function(){!a("#tool_wireframe").hasClass("push_button_pressed")?a("#tool_wireframe").addClass("push_button_pressed").removeClass("tool_button"):a("#tool_wireframe").removeClass("push_button_pressed").addClass("tool_button");ba.toggleClass("wireframe");if(!ec){var g=a("#wireframe_rules");g.length?g.empty():a('').appendTo("head");m()}},m=function(){if(!ec){var g="#workarea.wireframe #svgcontent * { stroke-width: "+1/l.getZoom()+ -"px; }";a("#wireframe_rules").text(ba.hasClass("wireframe")?g:"")}},i=function(g,p){if(!La){La=true;a("#save_output_btns").toggle(!!p);a("#tool_source_back").toggle(!p);var D=Ob=l.getSvgString();a("#svg_source_textarea").val(D);a("#svg_source_editor").fadeIn();h();a("#svg_source_textarea").focus()}};a("#svg_docprops_container, #svg_prefs_container").draggable({cancel:"button,fieldset",containment:"window"});var z=function(){if(!Ha){Ha=true;a("#image_save_opts input").val([e.img_save]);var g=l.getResolution(); -if(f.baseUnit!=="px"){g.w=svgedit.units.convertUnit(g.w)+f.baseUnit;g.h=svgedit.units.convertUnit(g.h)+f.baseUnit}a("#canvas_width").val(g.w);a("#canvas_height").val(g.h);a("#canvas_title").val(l.getDocumentTitle());a("#svg_docprops").show()}},o=function(){if(!ta){ta=true;a("#main_menu").hide();var g=a("#bg_blocks div"),p=a.pref("bkgd_color"),D=a.pref("bkgd_url");g.each(function(){var K=a(this),M=K.css("background-color")==p;K.toggleClass("cur_background",M);M&&a("#canvas_bg_url").removeClass("cur_background")}); -p||g.eq(0).addClass("cur_background");D&&a("#canvas_bg_url").val(D);a("grid_snapping_step").attr("value",f.snappingStep);f.gridSnapping==true?a("#grid_snapping_on").attr("checked","checked"):a("#grid_snapping_on").removeAttr("checked");a("#svg_prefs").show()}},h=function(){var g=a("#svg_source_container").height()-80;a("#svg_source_textarea").css("height",g)},q=function(){if(La){var g=function(){l.clearSelection();W();c();Xa();w();Ka.fill.prep();Ka.stroke.prep()};l.setSvgString(a("#svg_source_textarea").val())? -g():a.confirm(k.notification.QerrorsRevertToSource,function(p){if(!p)return false;g()});sb()}},w=function(g){g=g||l.getDocumentTitle();g=wa+(g?": "+g:"");a("title:first").text(g)},u=function(){var g=a("#canvas_title").val();w(g);l.setDocumentTitle(g);g=a("#canvas_width");var p=g.val(),D=a("#canvas_height"),K=D.val();if(p!="fit"&&!svgedit.units.isValidUnit("width",p)){a.alert(k.notification.invalidAttrValGiven);g.parent().addClass("error");return false}g.parent().removeClass("error");if(K!="fit"&& -!svgedit.units.isValidUnit("height",K)){a.alert(k.notification.invalidAttrValGiven);D.parent().addClass("error");return false}D.parent().removeClass("error");if(!l.setResolution(p,K)){a.alert(k.notification.noContentToFitTo);return false}e.img_save=a("#image_save_opts :checked").val();a.pref("img_save",e.img_save);qb();Y()},t=function(){var g=a("#bg_blocks div.cur_background").css("background-color")||"#FFF";A(g,a("#canvas_bg_url").val());g=a("#lang_select").val();g!=e.lang&&s.putLocale(g);I(a("#iconsize").val()); -f.gridSnapping=a("#grid_snapping_on")[0].checked;f.snappingStep=a("#grid_snapping_step").val();f.showRulers=a("#show_rulers")[0].checked;a("#rulers").toggle(f.showRulers);f.showRulers&&qa();f.baseUnit=a("#base_unit").val();l.setConfig(f);qb();R()},C=s.setIcon=function(g,p){var D=typeof p==="string"?a.getSvgIcon(p,true):p.clone();D?a(g).empty().append(D):console.log("NOTE: Icon image missing: "+p)},E;E=function(){var g=/^(Moz|Webkit|Khtml|O|ms|Icab)(?=[A-Z])/,p=document.getElementsByTagName("script")[0], -D;for(D in p.style)if(g.test(D))return D.match(g)[0];if("WebkitOpacity"in p.style)return"Webkit";if("KhtmlOpacity"in p.style)return"Khtml";return""}();var H=function(g,p){E.toLowerCase();var D=["top","left","bottom","right"];g.each(function(){for(var K=a(this),M=K.outerWidth()*(p-1),Q=K.outerHeight()*(p-1),U=0;U<4;U++){var X=D[U],ga=K.data("orig_margin-"+X);if(ga==null){ga=parseInt(K.css("margin-"+X));K.data("orig_margin-"+X,ga)}ga=ga*p;if(X==="right")ga+=M;else if(X==="bottom")ga+=Q;K.css("margin-"+ -X,ga)}})},I=s.setIconSize=function(g,p){if(!(g==e.size&&!p)){console.log("size",g);var D=a("#tools_top .toolset, #editor_panel > *, #history_panel > *,\t\t\t\t#main_button, #tools_left > *, #path_node_panel > *, #multiselected_panel > *,\t\t\t\t#g_panel > *, #tool_font_size > *, .tools_flyout"),K=1;K=typeof g=="number"?g:{s:0.75,m:1,l:1.25,xl:1.5}[g];s.tool_scale=ac=K;Fb();var M=D.parents(":hidden");M.css("visibility","hidden").show();H(D,K);M.css("visibility","visible").hide();a.pref("iconsize", -g);a("#iconsize").val(g);M={"#tools_top":{left:50,height:72},"#tools_left":{width:31,top:74},"div#workarea":{left:38,top:74}};D=a("#tool_size_rules");if(D.length)D.empty();else D=a('').appendTo("head");if(g!="m"){var Q="";a.each(M,function(U,X){U="#svg_editor "+U.replace(/,/g,", #svg_editor");Q+=U+"{";a.each(X,function(ga,ea){if(typeof ea==="number")var ma=ea*K+"px";else if(ea[g]||ea.all)ma=ea[g]||ea.all;Q+=ga+":"+ma+";"});Q+="}"});M="-"+E.toLowerCase()+"-";Q+= -"#tools_top .toolset, #editor_panel > *, #history_panel > *,\t\t\t\t#main_button, #tools_left > *, #path_node_panel > *, #multiselected_panel > *,\t\t\t\t#g_panel > *, #tool_font_size > *, .tools_flyout{"+M+"transform: scale("+K+");} #svg_editor div.toolset .toolset {"+M+"transform: scale(1); margin: 1px !important;} #svg_editor .ui-slider {"+M+"transform: scale("+1/K+");}";D.text(Q)}Fb()}},P=function(){a("#dialog_box").hide();if(!La&&!Ha&&!ta)oa&&l.leaveContext();else{if(La)Ob!==a("#svg_source_textarea").val()? -a.confirm(k.notification.QignoreSourceChanges,function(g){g&&W()}):W();else if(Ha)Y();else ta&&R();V()}},W=function(){a("#svg_source_editor").hide();La=false;a("#svg_source_textarea").blur()},Y=function(){a("#svg_docprops").hide();a("#canvas_width,#canvas_height").removeAttr("disabled");a("#resolution")[0].selectedIndex=0;a("#image_save_opts input").val([e.img_save]);Ha=false},R=function(){a("#svg_prefs").hide();ta=false},T={width:a(window).width(),height:a(window).height()},V=a.noop,ca;svgedit.browser.isIE()&& -function(){V=function(){if(ba[0].scrollLeft===0&&ba[0].scrollTop===0){ba[0].scrollLeft=ca.left;ba[0].scrollTop=ca.top}};ca={left:ba[0].scrollLeft,top:ba[0].scrollTop};a(window).resize(V);svgEditor.ready(function(){setTimeout(function(){V()},500)});ba.scroll(function(){ca={left:ba[0].scrollLeft,top:ba[0].scrollTop}})}();a(window).resize(function(){La&&h();a.each(T,function(g,p){var D=a(window)[g]();ba[0]["scroll"+(g==="width"?"Left":"Top")]-=(D-p)/2;T[g]=D})});(function(){ba.scroll(function(){if(a("#ruler_x").length!= -0)a("#ruler_x")[0].scrollLeft=ba[0].scrollLeft;if(a("#ruler_y").length!=0)a("#ruler_y")[0].scrollTop=ba[0].scrollTop})})();a("#url_notice").click(function(){a.alert(this.title)});a("#change_image_url").click(O);(function(){var g=["clear","open","save","source","delete","delete_multi","paste","clone","clone_multi","move_top","move_bottom"],p="";a.each(g,function(D,K){p+="#tool_"+K+(D==g.length-1?",":"")});a(p).mousedown(function(){a(this).addClass("tool_button_current")}).bind("mousedown mouseout", -function(){a(this).removeClass("tool_button_current")});a("#tool_undo, #tool_redo").mousedown(function(){a(this).hasClass("disabled")||a(this).addClass("tool_button_current")}).bind("mousedown mouseout",function(){a(this).removeClass("tool_button_current")})})();if(ja&&!window.opera){aa=["tool_clear","tool_save","tool_source","tool_undo","tool_redo","tool_clone"];for(ja=aa.length;ja--;){var Ia=document.getElementById(aa[ja]);if(Ia!=null){var Wa=Ia.title,kb=Wa.indexOf("Ctrl+");Ia.title=[Wa.substr(0, -kb),"Cmd+",Wa.substr(kb+5)].join("")}}}var ab=function(g){var p=g.attr("id")=="stroke_color"?"stroke":"fill",D=Ka[p].paint,K=p=="stroke"?"Pick a Stroke Paint and Opacity":"Pick a Fill Paint and Opacity";g=g.offset();a("#color_picker").draggable({cancel:".jGraduate_tabs, .jGraduate_colPick, .jGraduate_gradPick, .jPicker",containment:"window"}).css(f.colorPickerCSS||{left:g.left-140,bottom:40}).jGraduate({paint:D,window:{pickerTitle:K},images:{clientPath:f.jGraduatePath},newstop:"inverse"},function(M){D= -new a.jGraduate.Paint(M);Ka[p].setPaint(D);l.setPaint(p,D);a("#color_picker").hide()},function(){a("#color_picker").hide()})},bb=function(){var g=l.getColor("fill")=="none",p=l.getColor("stroke")=="none",D=["#tools_rect .tool_button","#tools_ellipse .tool_button","#tool_text","#tool_path"];if(p&&g)for(var K in D){var M=D[K];a(M).hasClass("tool_button_current")&&Hb();a(M).addClass("disabled")}else for(K in D){M=D[K];a(M).removeClass("disabled")}l.runExtensions("toolButtonStateUpdate",{nofill:g,nostroke:p}); -a(".tools_flyout").each(function(){var Q=a("#"+this.id+"_show"),U=false;a(this).children().each(function(){a(this).hasClass("disabled")||(U=true)});Q.toggleClass("disabled",!U)});Oa()};aa=function(g,p){var D=f[p==="fill"?"initFill":"initStroke"],K=(new DOMParser).parseFromString('\t\t\t\t\t',"text/xml").documentElement;K=a(g)[0].appendChild(document.importNode(K, -true));K.setAttribute("width",16.5);this.rect=K.firstChild;this.defs=K.getElementsByTagName("defs")[0];this.grad=this.defs.firstChild;this.paint=new a.jGraduate.Paint({solidColor:D.color});this.type=p;this.setPaint=function(M,Q){this.paint=M;var U="none",X=M.type,ga=M.alpha/100;switch(X){case "solidColor":U=M[X]!="none"?"#"+M[X]:M[X];break;case "linearGradient":case "radialGradient":this.defs.removeChild(this.grad);this.grad=this.defs.appendChild(M[X]);U="url(#"+(this.grad.id="gradbox_"+this.type)+ -")"}this.rect.setAttribute("fill",U);this.rect.setAttribute("opacity",ga);if(Q){l.setColor(this.type,paintColor,true);l.setPaintOpacity(this.type,paintOpacity,true)}};this.update=function(M){if(ha){var Q=this.type;switch(ha.tagName){case "use":case "image":case "foreignObject":return;case "g":case "a":for(var U=null,X=ha.getElementsByTagName("*"),ga=0,ea=X.length;ga300)g=300-D;else if(D+g<2)g=2-D;if(g!=0){wb-=g;D=a("#layerpanel");ba.css("right",parseInt(ba.css("right"))+g);p.css("width", -parseInt(p.css("width"))+g);D.css("width",parseInt(D.css("width"))+g);p=a("#ruler_x");p.css("right",parseInt(p.css("right"))+g)}}};a("#sidepanel_handle").mousedown(function(g){wb=g.pageX;a(window).mousemove(pc);nc=false;setTimeout(function(){nc=true},20)}).mouseup(function(){jc||oc();wb=-1;jc=false});a(window).mouseup(function(){wb=-1;jc=false;a("#svg_editor").unbind("mousemove",pc)});var oc=function(g){var p=parseInt(a("#sidepanels").css("width"));g=(p>2||g?2:150)-p;p=a("#sidepanels");var D=a("#layerpanel"), -K=a("#ruler_x");ba.css("right",parseInt(ba.css("right"))+g);p.css("width",parseInt(p.css("width"))+g);D.css("width",parseInt(D.css("width"))+g);K.css("right",parseInt(K.css("right"))+g)},qc=function(g){for(var p=Array(l.getCurrentDrawing().getNumLayers()),D=0;D'+Q+"":''+Q+"";g.append(U);p.append('")}if(M!== -undefined){M.clone();a("td.layervis",g).append(M.clone());a.resizeSvgIcons({"td.layervis .svg_icon":14})}a("#layerlist td.layername").mouseup(function(X){a("#layerlist tr.layer").removeClass("layersel");a(this.parentNode).addClass("layersel");l.setCurrentLayer(this.textContent);X.preventDefault()}).mouseover(function(){a(this).css({"font-style":"italic",color:"blue"});qc(this.textContent)}).mouseout(function(){a(this).css({"font-style":"normal",color:"black"});qc()});a("#layerlist td.layervis").click(function(){var X= -a(this.parentNode).prevAll().length;X=a("#layerlist tr.layer:eq("+X+") td.layername").text();var ga=a(this).hasClass("layerinvis");l.setLayerVisibility(X,ga);ga?a(this).removeClass("layerinvis"):a(this).addClass("layerinvis")});for(p=5-a("#layerlist tr.layer").size();p-- >0;)g.append('_')};Xa();a(window).bind("load resize",function(){ba.css("line-height",ba.height()+"px")});a("#resolution").change(function(){var g=a("#canvas_width,#canvas_height");if(this.selectedIndex)if(this.value== -"content")g.val("fit").attr("disabled","disabled");else{var p=this.value.split("x");a("#canvas_width").val(p[0]);a("#canvas_height").val(p[1]);g.removeAttr("disabled")}else a("#canvas_width").val()=="fit"&&g.removeAttr("disabled").val(100)});a("input,select").attr("autocomplete","off");var mc=function(){var g=[{sel:"#tool_select",fn:Hb,evt:"click",key:["S",true]},{sel:"#tool_fhpath",fn:Qb,evt:"click",key:["L",true]},{sel:"#tool_line",fn:Ab,evt:"click",key:["V",true]},{sel:"#tool_rect",fn:Sb,evt:"mouseup", -key:["R",true],parent:"#tools_rect",icon:"rect"},{sel:"#tool_square",fn:Bb,evt:"mouseup",parent:"#tools_rect",icon:"square"},{sel:"#tool_fhrect",fn:Ib,evt:"mouseup",parent:"#tools_rect",icon:"fh_rect"},{sel:"#tool_ellipse",fn:bc,evt:"mouseup",key:["E",true],parent:"#tools_ellipse",icon:"ellipse"},{sel:"#tool_circle",fn:kc,evt:"mouseup",parent:"#tools_ellipse",icon:"circle"},{sel:"#tool_fhellipse",fn:vb,evt:"mouseup",parent:"#tools_ellipse",icon:"fh_ellipse"},{sel:"#tool_path",fn:Tb,evt:"click",key:["P", -true]},{sel:"#tool_text",fn:rb,evt:"click",key:["T",true]},{sel:"#tool_image",fn:$a,evt:"mouseup"},{sel:"#tool_zoom",fn:Ga,evt:"mouseup",key:["",true]},{sel:"#tool_clear",fn:yb,evt:"mouseup",key:["N",true]},{sel:"#tool_save",fn:function(){La?q():l.save({images:e.img_save,round_digits:6})},evt:"mouseup",key:["",true]},{sel:"#tool_export",fn:xb,evt:"mouseup"},{sel:"#tool_open",fn:Ta,evt:"mouseup",key:["O",true]},{sel:"#tool_import",fn:hc,evt:"mouseup"},{sel:"#tool_source",fn:i,evt:"click",key:["U", -true]},{sel:"#tool_wireframe",fn:d,evt:"click",key:["W",true]},{sel:"#tool_source_cancel,#svg_source_overlay,#tool_docprops_cancel,#tool_prefs_cancel",fn:P,evt:"click",key:["esc",false,false],hidekey:true},{sel:"#tool_source_save",fn:q,evt:"click"},{sel:"#tool_docprops_save",fn:u,evt:"click"},{sel:"#tool_docprops",fn:z,evt:"mouseup"},{sel:"#tool_prefs_save",fn:t,evt:"click"},{sel:"#tool_prefs_option",fn:function(){o();return false},evt:"mouseup"},{sel:"#tool_delete,#tool_delete_multi",fn:Kb,evt:"click", -key:["del/backspace",true]},{sel:"#tool_reorient",fn:db,evt:"click"},{sel:"#tool_node_link",fn:Jb,evt:"click"},{sel:"#tool_node_clone",fn:Qa,evt:"click"},{sel:"#tool_node_delete",fn:sa,evt:"click"},{sel:"#tool_openclose_path",fn:Wb,evt:"click"},{sel:"#tool_add_subpath",fn:fc,evt:"click"},{sel:"#tool_move_top",fn:dc,evt:"click",key:"ctrl+shift+]"},{sel:"#tool_move_bottom",fn:Sa,evt:"click",key:"ctrl+shift+["},{sel:"#tool_topath",fn:Aa,evt:"click"},{sel:"#tool_make_link,#tool_make_link_multi",fn:ib, -evt:"click"},{sel:"#tool_undo",fn:gc,evt:"click",key:[na+"z",true]},{sel:"#tool_redo",fn:pb,evt:"click",key:[na+"y",true]},{sel:"#tool_clone,#tool_clone_multi",fn:Vb,evt:"click",key:["D",true]},{sel:"#tool_group",fn:Pa,evt:"click",key:["G",true]},{sel:"#tool_ungroup",fn:Pa,evt:"click"},{sel:"#tool_unlink_use",fn:Pa,evt:"click"},{sel:"[id^=tool_align]",fn:b,evt:"click"},{sel:"#tool_bold",fn:Mb,evt:"mousedown"},{sel:"#tool_italic",fn:lc,evt:"mousedown"},{sel:"#sidepanel_handle",fn:oc,key:["X"]},{sel:"#copy_save_done", -fn:P,evt:"click"},{key:"ctrl+left",fn:function(){Cb(0,1)}},{key:"ctrl+right",fn:function(){Cb(1,1)}},{key:"ctrl+shift+left",fn:function(){Cb(0,5)}},{key:"ctrl+shift+right",fn:function(){Cb(1,5)}},{key:"shift+O",fn:Nb},{key:"shift+P",fn:Eb},{key:[na+"up",true],fn:function(){c(2)}},{key:[na+"down",true],fn:function(){c(0.5)}},{key:[na+"]",true],fn:function(){cb("Up")}},{key:[na+"[",true],fn:function(){cb("Down")}},{key:["up",true],fn:function(){eb(0,-1)}},{key:["down",true],fn:function(){eb(0,1)}}, -{key:["left",true],fn:function(){eb(-1,0)}},{key:["right",true],fn:function(){eb(1,0)}},{key:"shift+up",fn:function(){eb(0,-10)}},{key:"shift+down",fn:function(){eb(0,10)}},{key:"shift+left",fn:function(){eb(-10,0)}},{key:"shift+right",fn:function(){eb(10,0)}},{key:["alt+up",true],fn:function(){l.cloneSelectedElements(0,-1)}},{key:["alt+down",true],fn:function(){l.cloneSelectedElements(0,1)}},{key:["alt+left",true],fn:function(){l.cloneSelectedElements(-1,0)}},{key:["alt+right",true],fn:function(){l.cloneSelectedElements(1, -0)}},{key:["alt+shift+up",true],fn:function(){l.cloneSelectedElements(0,-10)}},{key:["alt+shift+down",true],fn:function(){l.cloneSelectedElements(0,10)}},{key:["alt+shift+left",true],fn:function(){l.cloneSelectedElements(-10,0)}},{key:["alt+shift+right",true],fn:function(){l.cloneSelectedElements(10,0)}},{key:"A",fn:function(){l.selectAllInCurrentLayer()}},{key:na+"shift+z",fn:pb},{key:na+"x",fn:Ub},{key:na+"c",fn:cc},{key:na+"v",fn:Lb},{key:["esc",true,true],fn:l.clearSelection}],p={"4/Shift+4":"#tools_rect_show", -"5/Shift+5":"#tools_ellipse_show"};return{setAll:function(){var D={};a.each(g,function(K,M){if(M.sel){var Q=a(M.sel);if(Q.length==0)return true;if(M.evt){if(svgedit.browser.isTouch()&&M.evt==="click")M.evt="mousedown";Q[M.evt](M.fn)}if(M.parent&&a(M.parent+"_show").length!=0){var U=a(M.parent);U.length||(U=tb(M.parent.substr(1)));U.append(Q);a.isArray(D[M.parent])||(D[M.parent]=[]);D[M.parent].push(M)}}if(M.key){var X=M.fn,ga=false;if(a.isArray(M.key)){U=M.key[0];if(M.key.length>1)ga=M.key[1]}else U= -M.key;U+="";a.each(U.split("/"),function(ma,za){a(document).bind("keydown",za,function(Ea){X();ga&&Ea.preventDefault();return false})});if(M.sel&&!M.hidekey&&Q.attr("title")){var ea=Q.attr("title").split("[")[0]+" ("+U+")";p[U]=M.sel;Q.parents("#main_menu").length||Q.attr("title",ea)}}});Pb(D);a(".attr_changer, #image_url").bind("keydown","return",function(K){a(this).change();K.preventDefault()});a(window).bind("keydown","tab",function(K){if(Da==="canvas"){K.preventDefault();Eb()}}).bind("keydown", -"shift+tab",function(K){if(Da==="canvas"){K.preventDefault();Nb()}});a("#tool_zoom").dblclick(ia)},setTitles:function(){a.each(p,function(D,K){var M=a(K).parents("#main_menu").length;a(K).each(function(){var Q=M?a(this).text().split(" [")[0]:this.title.split(" [")[0],U="";a.each(D.split("/"),function(X,ga){var ea=ga.split("+"),ma="";if(ea.length>1){ma=ea[0]+"+";ga=ea[1]}U+=(X?"/":"")+ma+(k["key_"+ga]||ga)});if(M)this.lastChild.textContent=Q+" ["+U+"]";else this.title=Q+" ["+U+"]"})})},getButtonData:function(D){var K; -a.each(g,function(M,Q){if(Q.sel===D)K=Q});return K}}}();mc.setAll();s.ready(function(){var g=f.initTool,p=a("#tools_left, #svg_editor .tools_flyout"),D=p.find("#tool_"+g);g=p.find("#"+g);(D.length?D:g.length?g:a("#tool_select")).click().mouseup();f.wireframe&&a("#tool_wireframe").click();f.showlayers&&oc();a("#rulers").toggle(!!f.showRulers);if(f.showRulers)a("#show_rulers")[0].checked=true;if(f.gridSnapping)a("#grid_snapping_on")[0].checked=true;f.baseUnit&&a("#base_unit").val(f.baseUnit);f.snappingStep&& -a("#grid_snapping_step").val(f.snappingStep)});a("#rect_rx").SpinButton({min:0,max:1E3,step:1,callback:function(g){l.setRectRadius(g.value)}});a("#stroke_width").SpinButton({min:0,max:99,step:1,smallStep:0.1,callback:function(g){var p=g.value;if(p==0&&ha&&["line","polyline"].indexOf(ha.nodeName)>=0)p=g.value=1;l.setStrokeWidth(p)}});a("#angle").SpinButton({min:-180,max:180,step:5,callback:function(g){l.setRotationAngle(g.value);a("#tool_reorient").toggleClass("disabled",g.value==0)}});a("#font_size").SpinButton({step:1, -min:0.001,stepfunc:function(g,p){var D=g.value-0,K=D+p,M=K>=D;if(p===0)return D;return D>=24?M?Math.round(D*1.1):Math.round(D/1.1):D<=1?M?D*2:D/2:K},callback:function(g){l.setFontSize(g.value)}});a("#group_opacity").SpinButton({step:5,min:0,max:100,callback:ob});a("#blur").SpinButton({step:0.1,min:0,max:10,callback:Ua});a("#zoom").SpinButton({min:0.001,max:1E4,step:50,stepfunc:function(g,p){var D=g.value-0;if(D===0)return 100;var K=D+p;if(p===0)return D;return D>=100?K:K>=D?D*2:D/2},callback:Za}).val(l.getZoom()* -100);a("#workarea").contextMenu({menu:"cmenu_canvas",inSpeed:0},function(g){switch(g){case "delete":Kb();break;case "cut":Ub();break;case "copy":cc();break;case "paste":l.pasteElements();break;case "paste_in_place":l.pasteElements("in_place");break;case "group":l.groupSelectedElements();break;case "ungroup":l.ungroupSelectedElement();break;case "move_front":dc();break;case "move_up":cb("Up");break;case "move_down":cb("Down");break;case "move_back":Sa();break;default:svgedit.contextmenu&&svgedit.contextmenu.hasCustomHandler(g)&& -svgedit.contextmenu.getCustomHandler(g).call()}l.clipBoard.length&&gb.enableContextMenuItems("#paste,#paste_in_place")});aa=function(g){switch(g){case "dupe":N();break;case "delete":Z();break;case "merge_down":if(a("#layerlist tr.layersel").index()!=l.getCurrentDrawing().getNumLayers()-1){l.mergeLayer();ra();Xa()}break;case "merge_all":l.mergeAllLayers();ra();Xa()}};a("#layerlist").contextMenu({menu:"cmenu_layers",inSpeed:0},aa);a("#layer_moreopts").contextMenu({menu:"cmenu_layers",inSpeed:0,allowLeft:true}, -aa);a(".contextMenu li").mousedown(function(g){g.preventDefault()});a("#cmenu_canvas li").disableContextMenu();gb.enableContextMenuItems("#delete,#cut,#copy");window.onbeforeunload=function(){if("localStorage"in window){window.localStorage.setItem("svgedit-"+s.curConfig.canvasName,l.getSvgString());s.show_save_warning=false}if(Ra.getUndoStackSize()===0)s.show_save_warning=false;if(!f.no_save_warning&&s.show_save_warning)return k.notification.unsavedChanges};s.openPrep=function(g){a("#main_menu").hide(); -Ra.getUndoStackSize()===0?g(true):a.confirm(k.notification.QwantToOpen,g)};if(window.FileReader){aa=function(g){g.stopPropagation();g.preventDefault();a("#workarea").removeAttr("style");a("#main_menu").hide();var p=null;if(p=g.type=="drop"?g.dataTransfer.files[0]:this.files[0])if(p.type.indexOf("image")!=-1)if(p.type.indexOf("svg")!=-1){g=new FileReader;g.onloadend=function(D){l.importSvgString(D.target.result,true);l.ungroupSelectedElement();l.ungroupSelectedElement();l.groupSelectedElements();l.alignSelectedElements("m", -"page");l.alignSelectedElements("c","page")};g.readAsText(p)}else{g=new FileReader;g.onloadend=function(D){insertNewImage=function(U,X){var ga=l.addSvgElementFromJson({element:"image",attr:{x:0,y:0,width:U,height:X,id:l.getNextId(),style:"pointer-events:inherit"}});l.setHref(ga,D.target.result);l.selectOnly([ga]);l.alignSelectedElements("m","page");l.alignSelectedElements("c","page");ra()};var K=100,M=100,Q=new Image;Q.src=D.target.result;Q.style.opacity=0;Q.onload=function(){K=Q.offsetWidth;M=Q.offsetHeight; -insertNewImage(K,M)}};g.readAsDataURL(p)}};ba[0].addEventListener("dragenter",function(g){g.stopPropagation();g.preventDefault()},false);ba[0].addEventListener("dragover",function(g){g.stopPropagation();g.preventDefault()},false);ba[0].addEventListener("dragleave",function(g){g.stopPropagation();g.preventDefault()},false);ba[0].addEventListener("drop",aa,false);ja=a('').change(function(){var g=this;s.openPrep(function(p){if(p){l.clear();if(g.files.length==1){p=new FileReader;p.onloadend= -function(D){J(D.target.result);qb()};p.readAsText(g.files[0])}}})});a("#tool_open").show().prepend(ja);aa=a('').change(aa);a("#tool_import").show().prepend(aa)}var qb=s.updateCanvas=function(g,p){var D=ba.width(),K=ba.height(),M=D,Q=K,U=l.getZoom(),X=a("#svgcanvas"),ga={x:ba[0].scrollLeft+M/2,y:ba[0].scrollTop+Q/2},ea=f.canvas_expansion;D=Math.max(M,l.contentW*U*ea);K=Math.max(Q,l.contentH*U*ea);D==M&&K==Q?ba.css("overflow","hidden"):ba.css("overflow","scroll");ea=X.height()/2; -var ma=X.width()/2;X.width(D).height(K);var za=K/2,Ea=D/2,S=l.updateCanvas(D,K),Ba=Ea/ma;D=D/2-M/2;K=K/2-Q/2;if(p){p.x+=S.x;p.y+=S.y}else p={x:Ea+(ga.x-ma)*Ba,y:za+(ga.y-ea)*Ba};if(g)if(l.contentW>ba.width()){ba[0].scrollLeft=S.x-10;ba[0].scrollTop=S.y-10}else{ba[0].scrollLeft=D;ba[0].scrollTop=K}else{ba[0].scrollLeft=p.x-M/2;ba[0].scrollTop=p.y-Q/2}if(f.showRulers){qa(X,U);ba.scroll()}},Xb=[];for(ja=0.1;ja<1E5;ja*=10){Xb.push(1*ja);Xb.push(2*ja);Xb.push(5*ja)}qb(true);try{var rc=function(g){if(window.JSON&& -JSON.stringify)return JSON.stringify(g);var p=arguments.callee;if(typeof g=="boolean"||typeof g=="number")return g+"";else if(typeof g=="string")return'"'+g.replace(/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,function(M){return"\\u"+("0000"+M.charCodeAt(0).toString(16)).slice(-4)})+'"';else if(g.length){for(var D=0;D");var l=J.shortcut||"";$("#cmenu_canvas").append("
    • "+J.label+""+l+"
    • ")}});svgedit.contextmenu.resetCustomMenus=function(){a.contextMenuExtensions= -{}};svgedit.contextmenu.add=function(J){if(J&&J.id&&J.label&&J.action&&typeof J.action=="function")if(J.id in a.contextMenuExtensions)console.error('Cannot add extension "'+J.id+'", an extension by that name already exists"');else{console.log("Registed contextmenu item: {id:"+J.id+", label:"+J.label+"}");a.contextMenuExtensions[J.id]=J}else console.error("Menu items must be defined and have at least properties: id, label, action, where action must be a function")};svgedit.contextmenu.hasCustomHandler= -function(J){return a.contextMenuExtensions[J]&&true};svgedit.contextmenu.getCustomHandler=function(J){return a.contextMenuExtensions[J].action}})();var svgEditor=function(a,J){function l(v,G,e){var f=a("#svg_editor").parent(),k;for(k in G){var n=G[k];n||console.log(k);if(e)k="#"+k;if(f.find(k).length){var F=f.find(k)[0];switch(v){case "content":for(var B=0;B elements. Each element should contain the markup of an SVG -icon. The element has an ID that should -correspond with the ID of the HTML element used on the page that should contain -or optionally be replaced by the icon. Additionally, one empty element should be -added at the end with id "svg_eof". - -2. Optionally create fallback raster images for each SVG icon. - -3. Include the jQuery and the SVG Icon Loader scripts on your page. - -4. Run $.svgIcons() when the document is ready: - -$.svgIcons( file [string], options [object literal]); - -File is the location of a local SVG or SVGz file. - -All options are optional and can include: - -- 'w (number)': The icon widths - -- 'h (number)': The icon heights - -- 'fallback (object literal)': List of raster images with each - key being the SVG icon ID to replace, and the value the image file name. - -- 'fallback_path (string)': The path to use for all images - listed under "fallback" - -- 'replace (boolean)': If set to true, HTML elements will be replaced by, - rather than include the SVG icon. - -- 'placement (object literal)': List with selectors for keys and SVG icon ids - as values. This provides a custom method of adding icons. - -- 'resize (object literal)': List with selectors for keys and numbers - as values. This allows an easy way to resize specific icons. - -- 'callback (function)': A function to call when all icons have been loaded. - Includes an object literal as its argument with as keys all icon IDs and the - icon as a jQuery object as its value. - -- 'id_match (boolean)': Automatically attempt to match SVG icon ids with - corresponding HTML id (default: true) - -- 'no_img (boolean)': Prevent attempting to convert the icon into an - element (may be faster, help for browser consistency) - -- 'svgz (boolean)': Indicate that the file is an SVGZ file, and thus not to - parse as XML. SVGZ files add compression benefits, but getting data from - them fails in Firefox 2 and older. - -5. To access an icon at a later point without using the callback, use this: - $.getSvgIcon(id (string)); - -This will return the icon (as jQuery object) with a given ID. - -6. To resize icons at a later point without using the callback, use this: - $.resizeSvgIcons(resizeOptions) (use the same way as the "resize" parameter) - - -Example usage #1: - -$(function() { - $.svgIcons('my_icon_set.svg'); // The SVG file that contains all icons - // No options have been set, so all icons will automatically be inserted - // into HTML elements that match the same IDs. -}); - -Example usage #2: - -$(function() { - $.svgIcons('my_icon_set.svg', { // The SVG file that contains all icons - callback: function(icons) { // Custom callback function that sets click - // events for each icon - $.each(icons, function(id, icon) { - icon.click(function() { - alert('You clicked on the icon with id ' + id); - }); - }); - } - }); //The SVG file that contains all icons -}); - -Example usage #3: - -$(function() { - $.svgIcons('my_icon_set.svgz', { // The SVGZ file that contains all icons - w: 32, // All icons will be 32px wide - h: 32, // All icons will be 32px high - fallback_path: 'icons/', // All fallback files can be found here - fallback: { - '#open_icon': 'open.png', // The "open.png" will be appended to the - // HTML element with ID "open_icon" - '#close_icon': 'close.png', - '#save_icon': 'save.png' - }, - placement: {'.open_icon','open'}, // The "open" icon will be added - // to all elements with class "open_icon" - resize: function() { - '#save_icon .svg_icon': 64 // The "save" icon will be resized to 64 x 64px - }, - - callback: function(icons) { // Sets background color for "close" icon - icons['close'].css('background','red'); - }, - - svgz: true // Indicates that an SVGZ file is being used - - }) -}); - -*/ - - -(function($) { - var svg_icons = {}, fixIDs; - - $.svgIcons = function(file, opts) { - var svgns = "http://www.w3.org/2000/svg", - xlinkns = "http://www.w3.org/1999/xlink", - icon_w = opts.w?opts.w : 24, - icon_h = opts.h?opts.h : 24, - elems, svgdoc, testImg, - icons_made = false, data_loaded = false, load_attempts = 0, - ua = navigator.userAgent, isOpera = !!window.opera, isSafari = (ua.indexOf('Safari/') > -1 && ua.indexOf('Chrome/')==-1), - data_pre = 'data:image/svg+xml;charset=utf-8;base64,'; - - if(opts.svgz) { - var data_el = $('').appendTo('body').hide(); - try { - svgdoc = data_el[0].contentDocument; - data_el.load(getIcons); - getIcons(0, true); // Opera will not run "load" event if file is already cached - } catch(err1) { - useFallback(); - } - } else { - var parser = new DOMParser(); - $.ajax({ - url: file, - dataType: 'string', - success: function(data) { - if(!data) { - $(useFallback); - return; - } - svgdoc = parser.parseFromString(data, "text/xml"); - $(function() { - getIcons('ajax'); - }); - }, - error: function(err) { - // TODO: Fix Opera widget icon bug - if(window.opera) { - $(function() { - useFallback(); - }); - } else { - if(err.responseText) { - svgdoc = parser.parseFromString(err.responseText, "text/xml"); - - if(!svgdoc.childNodes.length) { - $(useFallback); - } - $(function() { - getIcons('ajax'); - }); - } else { - $(useFallback); - } - } - } - }); - } - - function getIcons(evt, no_wait) { - if(evt !== 'ajax') { - if(data_loaded) return; - // Webkit sometimes says svgdoc is undefined, other times - // it fails to load all nodes. Thus we must make sure the "eof" - // element is loaded. - svgdoc = data_el[0].contentDocument; // Needed again for Webkit - var isReady = (svgdoc && svgdoc.getElementById('svg_eof')); - if(!isReady && !(no_wait && isReady)) { - load_attempts++; - if(load_attempts < 50) { - setTimeout(getIcons, 20); - } else { - useFallback(); - data_loaded = true; - } - return; - } - data_loaded = true; - } - - elems = $(svgdoc.firstChild).children(); //.getElementsByTagName('foreignContent'); - - if(!opts.no_img) { - var testSrc = data_pre + 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNzUiIGhlaWdodD0iMjc1Ij48L3N2Zz4%3D'; - - testImg = $(new Image()).attr({ - src: testSrc, - width: 0, - height: 0 - }).appendTo('body') - .load(function () { - // Safari 4 crashes, Opera and Chrome don't - makeIcons(true); - }).error(function () { - makeIcons(); - }); - } else { - setTimeout(function() { - if(!icons_made) makeIcons(); - },500); - } - } - - var setIcon = function(target, icon, id, setID) { - if(isOpera) icon.css('visibility','hidden'); - if(opts.replace) { - if(setID) icon.attr('id',id); - var cl = target.attr('class'); - if(cl) icon.attr('class','svg_icon '+cl); - target.replaceWith(icon); - } else { - - target.append(icon); - } - if(isOpera) { - setTimeout(function() { - icon.removeAttr('style'); - },1); - } - } - - var addIcon = function(icon, id) { - if(opts.id_match === undefined || opts.id_match !== false) { - setIcon(holder, icon, id, true); - } - svg_icons[id] = icon; - } - - function makeIcons(toImage, fallback) { - if(icons_made) return; - if(opts.no_img) toImage = false; - var holder; - - if(toImage) { - var temp_holder = $(document.createElement('div')); - temp_holder.hide().appendTo('body'); - } - if(fallback) { - var path = opts.fallback_path?opts.fallback_path:''; - $.each(fallback, function(id, imgsrc) { - holder = $('#' + id); - var icon = $(new Image()) - .attr({ - 'class':'svg_icon', - src: path + imgsrc, - 'width': icon_w, - 'height': icon_h, - 'alt': 'icon' - }); - - addIcon(icon, id); - }); - } else { - var len = elems.length; - for(var i = 0; i < len; i++) { - var elem = elems[i]; - var id = elem.id; - if(id === 'svg_eof') break; - holder = $('#' + id); - var svg = elem.getElementsByTagNameNS(svgns, 'svg')[0]; - var svgroot = document.createElementNS(svgns, "svg"); - svgroot.setAttributeNS(svgns, 'viewBox', [0,0,icon_w,icon_h].join(' ')); - - // Make flexible by converting width/height to viewBox - var w = svg.getAttribute('width'); - var h = svg.getAttribute('height'); - svg.removeAttribute('width'); - svg.removeAttribute('height'); - - var vb = svg.getAttribute('viewBox'); - if(!vb) { - svg.setAttribute('viewBox', [0,0,w,h].join(' ')); - } - - // Not using jQuery to be a bit faster - svgroot.setAttribute('xmlns', svgns); - svgroot.setAttribute('width', icon_w); - svgroot.setAttribute('height', icon_h); - svgroot.setAttribute("xmlns:xlink", xlinkns); - svgroot.setAttribute("class", 'svg_icon'); - - // Without cloning, Firefox will make another GET request. - // With cloning, causes issue in Opera/Win/Non-EN - if(!isOpera) svg = svg.cloneNode(true); - - svgroot.appendChild(svg); - - if(toImage) { - // Without cloning, Safari will crash - // With cloning, causes issue in Opera/Win/Non-EN - var svgcontent = isOpera?svgroot:svgroot.cloneNode(true); - temp_holder.empty().append(svgroot); - var str = data_pre + encode64(temp_holder.html()); - var icon = $(new Image()) - .attr({'class':'svg_icon', src:str}); - } else { - var icon = fixIDs($(svgroot), i); - } - addIcon(icon, id); - } - - } - - if(opts.placement) { - $.each(opts.placement, function(sel, id) { - if(!svg_icons[id]) return; - $(sel).each(function(i) { - var copy = svg_icons[id].clone(); - if(i > 0 && !toImage) copy = fixIDs(copy, i, true); - setIcon($(this), copy, id); - }) - }); - } - if(!fallback) { - if(toImage) temp_holder.remove(); - if(data_el) data_el.remove(); - if(testImg) testImg.remove(); - } - if(opts.resize) $.resizeSvgIcons(opts.resize); - icons_made = true; - - if(opts.callback) opts.callback(svg_icons); - } - - fixIDs = function(svg_el, svg_num, force) { - var defs = svg_el.find('defs'); - if(!defs.length) return svg_el; - - if(isOpera) { - var id_elems = defs.find('*').filter(function() { - return !!this.id; - }); - } else { - var id_elems = defs.find('[id]'); - } - - var all_elems = svg_el[0].getElementsByTagName('*'), len = all_elems.length; - - id_elems.each(function(i) { - var id = this.id; - var no_dupes = ($(svgdoc).find('#' + id).length <= 1); - if(isOpera) no_dupes = false; // Opera didn't clone svg_el, so not reliable - // if(!force && no_dupes) return; - var new_id = 'x' + id + svg_num + i; - this.id = new_id; - - var old_val = 'url(#' + id + ')'; - var new_val = 'url(#' + new_id + ')'; - - // Selector method, possibly faster but fails in Opera / jQuery 1.4.3 -// svg_el.find('[fill="url(#' + id + ')"]').each(function() { -// this.setAttribute('fill', 'url(#' + new_id + ')'); -// }).end().find('[stroke="url(#' + id + ')"]').each(function() { -// this.setAttribute('stroke', 'url(#' + new_id + ')'); -// }).end().find('use').each(function() { -// if(this.getAttribute('xlink:href') == '#' + id) { -// this.setAttributeNS(xlinkns,'href','#' + new_id); -// } -// }).end().find('[filter="url(#' + id + ')"]').each(function() { -// this.setAttribute('filter', 'url(#' + new_id + ')'); -// }); - - for(var i = 0; i < len; i++) { - var elem = all_elems[i]; - if(elem.getAttribute('fill') === old_val) { - elem.setAttribute('fill', new_val); - } - if(elem.getAttribute('stroke') === old_val) { - elem.setAttribute('stroke', new_val); - } - if(elem.getAttribute('filter') === old_val) { - elem.setAttribute('filter', new_val); - } - } - }); - return svg_el; - } - - function useFallback() { - if(file.indexOf('.svgz') != -1) { - var reg_file = file.replace('.svgz','.svg'); - if(window.console) { - console.log('.svgz failed, trying with .svg'); - } - $.svgIcons(reg_file, opts); - } else if(opts.fallback) { - makeIcons(false, opts.fallback); - } - } - - function encode64(input) { - // base64 strings are 4/3 larger than the original string - if(window.btoa) return window.btoa(input); - var _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - var output = new Array( Math.floor( (input.length + 2) / 3 ) * 4 ); - var chr1, chr2, chr3; - var enc1, enc2, enc3, enc4; - var i = 0, p = 0; - - do { - chr1 = input.charCodeAt(i++); - chr2 = input.charCodeAt(i++); - chr3 = input.charCodeAt(i++); - - enc1 = chr1 >> 2; - enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; - - if (isNaN(chr2)) { - enc3 = enc4 = 64; - } else if (isNaN(chr3)) { - enc4 = 64; - } - - output[p++] = _keyStr.charAt(enc1); - output[p++] = _keyStr.charAt(enc2); - output[p++] = _keyStr.charAt(enc3); - output[p++] = _keyStr.charAt(enc4); - } while (i < input.length); - - return output.join(''); - } - } - - $.getSvgIcon = function(id, uniqueClone) { - var icon = svg_icons[id]; - if(uniqueClone && icon) { - icon = fixIDs(icon, 0, true).clone(true); - } - return icon; - } - - $.resizeSvgIcons = function(obj) { - // FF2 and older don't detect .svg_icon, so we change it detect svg elems instead - var change_sel = !$('.svg_icon:first').length; - $.each(obj, function(sel, size) { - var arr = $.isArray(size); - var w = arr?size[0]:size, - h = arr?size[1]:size; - if(change_sel) { - sel = sel.replace(/\.svg_icon/g,'svg'); - } - $(sel).each(function() { - this.setAttribute('width', w); - this.setAttribute('height', h); - if(window.opera && window.widget) { - this.parentNode.style.width = w + 'px'; - this.parentNode.style.height = h + 'px'; - } - }); - }); - } - -})(jQuery); \ No newline at end of file diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgtransformlist.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgtransformlist.js deleted file mode 100644 index 2f0d6734..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgtransformlist.js +++ /dev/null @@ -1,291 +0,0 @@ -/** - * SVGTransformList - * - * Licensed under the MIT License - * - * Copyright(c) 2010 Alexis Deveria - * Copyright(c) 2010 Jeff Schiller - */ - -// Dependencies: -// 1) browser.js - -var svgedit = svgedit || {}; - -(function() { - -if (!svgedit.transformlist) { - svgedit.transformlist = {}; -} - -var svgroot = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - -// Helper function. -function transformToString(xform) { - var m = xform.matrix, - text = ""; - switch(xform.type) { - case 1: // MATRIX - text = "matrix(" + [m.a,m.b,m.c,m.d,m.e,m.f].join(",") + ")"; - break; - case 2: // TRANSLATE - text = "translate(" + m.e + "," + m.f + ")"; - break; - case 3: // SCALE - if (m.a == m.d) text = "scale(" + m.a + ")"; - else text = "scale(" + m.a + "," + m.d + ")"; - break; - case 4: // ROTATE - var cx = 0, cy = 0; - // this prevents divide by zero - if (xform.angle != 0) { - var K = 1 - m.a; - cy = ( K * m.f + m.b*m.e ) / ( K*K + m.b*m.b ); - cx = ( m.e - m.b * cy ) / K; - } - text = "rotate(" + xform.angle + " " + cx + "," + cy + ")"; - break; - } - return text; -}; - - -/** - * Map of SVGTransformList objects. - */ -var listMap_ = {}; - - -// ************************************************************************************** -// SVGTransformList implementation for Webkit -// These methods do not currently raise any exceptions. -// These methods also do not check that transforms are being inserted. This is basically -// implementing as much of SVGTransformList that we need to get the job done. -// -// interface SVGEditTransformList { -// attribute unsigned long numberOfItems; -// void clear ( ) -// SVGTransform initialize ( in SVGTransform newItem ) -// SVGTransform getItem ( in unsigned long index ) (DOES NOT THROW DOMException, INDEX_SIZE_ERR) -// SVGTransform insertItemBefore ( in SVGTransform newItem, in unsigned long index ) (DOES NOT THROW DOMException, INDEX_SIZE_ERR) -// SVGTransform replaceItem ( in SVGTransform newItem, in unsigned long index ) (DOES NOT THROW DOMException, INDEX_SIZE_ERR) -// SVGTransform removeItem ( in unsigned long index ) (DOES NOT THROW DOMException, INDEX_SIZE_ERR) -// SVGTransform appendItem ( in SVGTransform newItem ) -// NOT IMPLEMENTED: SVGTransform createSVGTransformFromMatrix ( in SVGMatrix matrix ); -// NOT IMPLEMENTED: SVGTransform consolidate ( ); -// } -// ************************************************************************************** -svgedit.transformlist.SVGTransformList = function(elem) { - this._elem = elem || null; - this._xforms = []; - // TODO: how do we capture the undo-ability in the changed transform list? - this._update = function() { - var tstr = ""; - var concatMatrix = svgroot.createSVGMatrix(); - for (var i = 0; i < this.numberOfItems; ++i) { - var xform = this._list.getItem(i); - tstr += transformToString(xform) + " "; - } - this._elem.setAttribute("transform", tstr); - }; - this._list = this; - this._init = function() { - // Transform attribute parser - var str = this._elem.getAttribute("transform"); - if(!str) return; - - // TODO: Add skew support in future - var re = /\s*((scale|matrix|rotate|translate)\s*\(.*?\))\s*,?\s*/; - var arr = []; - var m = true; - while(m) { - m = str.match(re); - str = str.replace(re,''); - if(m && m[1]) { - var x = m[1]; - var bits = x.split(/\s*\(/); - var name = bits[0]; - var val_bits = bits[1].match(/\s*(.*?)\s*\)/); - val_bits[1] = val_bits[1].replace(/(\d)-/g, "$1 -"); - var val_arr = val_bits[1].split(/[, ]+/); - var letters = 'abcdef'.split(''); - var mtx = svgroot.createSVGMatrix(); - $.each(val_arr, function(i, item) { - val_arr[i] = parseFloat(item); - if(name == 'matrix') { - mtx[letters[i]] = val_arr[i]; - } - }); - var xform = svgroot.createSVGTransform(); - var fname = 'set' + name.charAt(0).toUpperCase() + name.slice(1); - var values = name=='matrix'?[mtx]:val_arr; - - if (name == 'scale' && values.length == 1) { - values.push(values[0]); - } else if (name == 'translate' && values.length == 1) { - values.push(0); - } else if (name == 'rotate' && values.length == 1) { - values.push(0); - values.push(0); - } - xform[fname].apply(xform, values); - this._list.appendItem(xform); - } - } - }; - this._removeFromOtherLists = function(item) { - if (item) { - // Check if this transform is already in a transformlist, and - // remove it if so. - var found = false; - for (var id in listMap_) { - var tl = listMap_[id]; - for (var i = 0, len = tl._xforms.length; i < len; ++i) { - if(tl._xforms[i] == item) { - found = true; - tl.removeItem(i); - break; - } - } - if (found) { - break; - } - } - } - }; - - this.numberOfItems = 0; - this.clear = function() { - this.numberOfItems = 0; - this._xforms = []; - }; - - this.initialize = function(newItem) { - this.numberOfItems = 1; - this._removeFromOtherLists(newItem); - this._xforms = [newItem]; - }; - - this.getItem = function(index) { - if (index < this.numberOfItems && index >= 0) { - return this._xforms[index]; - } - throw {code: 1}; // DOMException with code=INDEX_SIZE_ERR - }; - - this.insertItemBefore = function(newItem, index) { - var retValue = null; - if (index >= 0) { - if (index < this.numberOfItems) { - this._removeFromOtherLists(newItem); - var newxforms = new Array(this.numberOfItems + 1); - // TODO: use array copying and slicing - for ( var i = 0; i < index; ++i) { - newxforms[i] = this._xforms[i]; - } - newxforms[i] = newItem; - for ( var j = i+1; i < this.numberOfItems; ++j, ++i) { - newxforms[j] = this._xforms[i]; - } - this.numberOfItems++; - this._xforms = newxforms; - retValue = newItem; - this._list._update(); - } - else { - retValue = this._list.appendItem(newItem); - } - } - return retValue; - }; - - this.replaceItem = function(newItem, index) { - var retValue = null; - if (index < this.numberOfItems && index >= 0) { - this._removeFromOtherLists(newItem); - this._xforms[index] = newItem; - retValue = newItem; - this._list._update(); - } - return retValue; - }; - - this.removeItem = function(index) { - if (index < this.numberOfItems && index >= 0) { - var retValue = this._xforms[index]; - var newxforms = new Array(this.numberOfItems - 1); - for (var i = 0; i < index; ++i) { - newxforms[i] = this._xforms[i]; - } - for (var j = i; j < this.numberOfItems-1; ++j, ++i) { - newxforms[j] = this._xforms[i+1]; - } - this.numberOfItems--; - this._xforms = newxforms; - this._list._update(); - return retValue; - } else { - throw {code: 1}; // DOMException with code=INDEX_SIZE_ERR - } - }; - - this.appendItem = function(newItem) { - this._removeFromOtherLists(newItem); - this._xforms.push(newItem); - this.numberOfItems++; - this._list._update(); - return newItem; - }; -}; - - -svgedit.transformlist.resetListMap = function() { - listMap_ = {}; -}; - -/** - * Removes transforms of the given element from the map. - * Parameters: - * elem - a DOM Element - */ -svgedit.transformlist.removeElementFromListMap = function(elem) { - if (elem.id && listMap_[elem.id]) { - delete listMap_[elem.id]; - } -}; - -// Function: getTransformList -// Returns an object that behaves like a SVGTransformList for the given DOM element -// -// Parameters: -// elem - DOM element to get a transformlist from -svgedit.transformlist.getTransformList = function(elem) { - if (!svgedit.browser.supportsNativeTransformLists()) { - var id = elem.id; - if(!id) { - // Get unique ID for temporary element - id = 'temp'; - } - var t = listMap_[id]; - if (!t || id == 'temp') { - listMap_[id] = new svgedit.transformlist.SVGTransformList(elem); - listMap_[id]._init(); - t = listMap_[id]; - } - return t; - } - else if (elem.transform) { - return elem.transform.baseVal; - } - else if (elem.gradientTransform) { - return elem.gradientTransform.baseVal; - } - else if (elem.patternTransform) { - return elem.patternTransform.baseVal; - } - - return null; -}; - - -})(); \ No newline at end of file diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgutils.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgutils.js deleted file mode 100644 index 7105c583..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/svgutils.js +++ /dev/null @@ -1,651 +0,0 @@ -/** - * Package: svgedit.utilities - * - * Licensed under the MIT License - * - * Copyright(c) 2010 Alexis Deveria - * Copyright(c) 2010 Jeff Schiller - */ - -// Dependencies: -// 1) jQuery -// 2) browser.js -// 3) svgtransformlist.js - -var svgedit = svgedit || {}; - -(function() { - -if (!svgedit.utilities) { - svgedit.utilities = {}; -} - -// Constants - -// String used to encode base64. -var KEYSTR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; -var SVGNS = 'http://www.w3.org/2000/svg'; -var XLINKNS = 'http://www.w3.org/1999/xlink'; -var XMLNS = "http://www.w3.org/XML/1998/namespace"; - -// Much faster than running getBBox() every time -var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'; -var visElems_arr = visElems.split(','); -//var hidElems = 'clipPath,defs,desc,feGaussianBlur,filter,linearGradient,marker,mask,metadata,pattern,radialGradient,stop,switch,symbol,title,textPath'; - -var editorContext_ = null; -var domdoc_ = null; -var domcontainer_ = null; -var svgroot_ = null; - -svgedit.utilities.init = function(editorContext) { - editorContext_ = editorContext; - domdoc_ = editorContext.getDOMDocument(); - domcontainer_ = editorContext.getDOMContainer(); - svgroot_ = editorContext.getSVGRoot(); -}; - -// Function: svgedit.utilities.toXml -// Converts characters in a string to XML-friendly entities. -// -// Example: "&" becomes "&" -// -// Parameters: -// str - The string to be converted -// -// Returns: -// The converted string -svgedit.utilities.toXml = function(str) { - return $('

      ').text(str).html(); -}; - -// Function: svgedit.utilities.fromXml -// Converts XML entities in a string to single characters. -// Example: "&" becomes "&" -// -// Parameters: -// str - The string to be converted -// -// Returns: -// The converted string -svgedit.utilities.fromXml = function(str) { - return $('

      ').html(str).text(); -}; - -// This code was written by Tyler Akins and has been placed in the -// public domain. It would be nice if you left this header intact. -// Base64 code from Tyler Akins -- http://rumkin.com - -// schiller: Removed string concatenation in favour of Array.join() optimization, -// also precalculate the size of the array needed. - -// Function: svgedit.utilities.encode64 -// Converts a string to base64 -svgedit.utilities.encode64 = function(input) { - // base64 strings are 4/3 larger than the original string -// input = svgedit.utilities.encodeUTF8(input); // convert non-ASCII characters - input = svgedit.utilities.convertToXMLReferences(input); - if(window.btoa) return window.btoa(input); // Use native if available - var output = new Array( Math.floor( (input.length + 2) / 3 ) * 4 ); - var chr1, chr2, chr3; - var enc1, enc2, enc3, enc4; - var i = 0, p = 0; - - do { - chr1 = input.charCodeAt(i++); - chr2 = input.charCodeAt(i++); - chr3 = input.charCodeAt(i++); - - enc1 = chr1 >> 2; - enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; - - if (isNaN(chr2)) { - enc3 = enc4 = 64; - } else if (isNaN(chr3)) { - enc4 = 64; - } - - output[p++] = KEYSTR.charAt(enc1); - output[p++] = KEYSTR.charAt(enc2); - output[p++] = KEYSTR.charAt(enc3); - output[p++] = KEYSTR.charAt(enc4); - } while (i < input.length); - - return output.join(''); -}; - -// Function: svgedit.utilities.decode64 -// Converts a string from base64 -svgedit.utilities.decode64 = function(input) { - if(window.atob) return window.atob(input); - var output = ""; - var chr1, chr2, chr3 = ""; - var enc1, enc2, enc3, enc4 = ""; - var i = 0; - - // remove all characters that are not A-Z, a-z, 0-9, +, /, or = - input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); - - do { - enc1 = KEYSTR.indexOf(input.charAt(i++)); - enc2 = KEYSTR.indexOf(input.charAt(i++)); - enc3 = KEYSTR.indexOf(input.charAt(i++)); - enc4 = KEYSTR.indexOf(input.charAt(i++)); - - chr1 = (enc1 << 2) | (enc2 >> 4); - chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); - chr3 = ((enc3 & 3) << 6) | enc4; - - output = output + String.fromCharCode(chr1); - - if (enc3 != 64) { - output = output + String.fromCharCode(chr2); - } - if (enc4 != 64) { - output = output + String.fromCharCode(chr3); - } - - chr1 = chr2 = chr3 = ""; - enc1 = enc2 = enc3 = enc4 = ""; - - } while (i < input.length); - return unescape(output); -}; - -// Currently not being used, so commented out for now -// based on http://phpjs.org/functions/utf8_encode:577 -// codedread:does not seem to work with webkit-based browsers on OSX -// "encodeUTF8": function(input) { -// //return unescape(encodeURIComponent(input)); //may or may not work -// var output = ''; -// for (var n = 0; n < input.length; n++){ -// var c = input.charCodeAt(n); -// if (c < 128) { -// output += input[n]; -// } -// else if (c > 127) { -// if (c < 2048){ -// output += String.fromCharCode((c >> 6) | 192); -// } -// else { -// output += String.fromCharCode((c >> 12) | 224) + String.fromCharCode((c >> 6) & 63 | 128); -// } -// output += String.fromCharCode((c & 63) | 128); -// } -// } -// return output; -// }, - -// Function: svgedit.utilities.convertToXMLReferences -// Converts a string to use XML references -svgedit.utilities.convertToXMLReferences = function(input) { - var output = ''; - for (var n = 0; n < input.length; n++){ - var c = input.charCodeAt(n); - if (c < 128) { - output += input[n]; - } else if(c > 127) { - output += ("&#" + c + ";"); - } - } - return output; -}; - -// Function: svgedit.utilities.text2xml -// Cross-browser compatible method of converting a string to an XML tree -// found this function here: http://groups.google.com/group/jquery-dev/browse_thread/thread/c6d11387c580a77f -svgedit.utilities.text2xml = function(sXML) { - if(sXML.indexOf('= 0) { - sXML = sXML.replace(/<(\/?)svg:/g, '<$1').replace('xmlns:svg', 'xmlns'); - } - - var out; - try{ - var dXML = (window.DOMParser)?new DOMParser():new ActiveXObject("Microsoft.XMLDOM"); - dXML.async = false; - } catch(e){ - throw new Error("XML Parser could not be instantiated"); - }; - try{ - if(dXML.loadXML) out = (dXML.loadXML(sXML))?dXML:false; - else out = dXML.parseFromString(sXML, "text/xml"); - } - catch(e){ throw new Error("Error parsing XML string"); }; - return out; -}; - -// Function: svgedit.utilities.bboxToObj -// Converts a SVGRect into an object. -// -// Parameters: -// bbox - a SVGRect -// -// Returns: -// An object with properties names x, y, width, height. -svgedit.utilities.bboxToObj = function(bbox) { - return { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height - } -}; - -// Function: svgedit.utilities.walkTree -// Walks the tree and executes the callback on each element in a top-down fashion -// -// Parameters: -// elem - DOM element to traverse -// cbFn - Callback function to run on each element -svgedit.utilities.walkTree = function(elem, cbFn){ - if (elem && elem.nodeType == 1) { - cbFn(elem); - var i = elem.childNodes.length; - while (i--) { - svgedit.utilities.walkTree(elem.childNodes.item(i), cbFn); - } - } -}; - -// Function: svgedit.utilities.walkTreePost -// Walks the tree and executes the callback on each element in a depth-first fashion -// TODO: FIXME: Shouldn't this be calling walkTreePost? -// -// Parameters: -// elem - DOM element to traverse -// cbFn - Callback function to run on each element -svgedit.utilities.walkTreePost = function(elem, cbFn) { - if (elem && elem.nodeType == 1) { - var i = elem.childNodes.length; - while (i--) { - svgedit.utilities.walkTree(elem.childNodes.item(i), cbFn); - } - cbFn(elem); - } -}; - -// Function: svgedit.utilities.getUrlFromAttr -// Extracts the URL from the url(...) syntax of some attributes. -// Three variants: -// * -// * -// * -// -// Parameters: -// attrVal - The attribute value as a string -// -// Returns: -// String with just the URL, like someFile.svg#foo -svgedit.utilities.getUrlFromAttr = function(attrVal) { - if (attrVal) { - // url("#somegrad") - if (attrVal.indexOf('url("') === 0) { - return attrVal.substring(5,attrVal.indexOf('"',6)); - } - // url('#somegrad') - else if (attrVal.indexOf("url('") === 0) { - return attrVal.substring(5,attrVal.indexOf("'",6)); - } - else if (attrVal.indexOf("url(") === 0) { - return attrVal.substring(4,attrVal.indexOf(')')); - } - } - return null; -}; - -// Function: svgedit.utilities.getHref -// Returns the given element's xlink:href value -svgedit.utilities.getHref = function(elem) { - return elem.getAttributeNS(XLINKNS, "href"); -} - -// Function: svgedit.utilities.setHref -// Sets the given element's xlink:href value -svgedit.utilities.setHref = function(elem, val) { - elem.setAttributeNS(XLINKNS, "xlink:href", val); -} - -// Function: findDefs -// Parameters: -// svgElement - The element. -// -// Returns: -// The document's element, create it first if necessary -svgedit.utilities.findDefs = function(svgElement) { - var svgElement = editorContext_.getSVGContent().documentElement; - var defs = svgElement.getElementsByTagNameNS(SVGNS, "defs"); - if (defs.length > 0) { - defs = defs[0]; - } - else { - // first child is a comment, so call nextSibling - defs = svgElement.insertBefore( svgElement.ownerDocument.createElementNS(SVGNS, "defs" ), svgElement.firstChild.nextSibling); - } - return defs; -}; - -// TODO(codedread): Consider moving the next to functions to bbox.js - -// Function: svgedit.utilities.getPathBBox -// Get correct BBox for a path in Webkit -// Converted from code found here: -// http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html -// -// Parameters: -// path - The path DOM element to get the BBox for -// -// Returns: -// A BBox-like object -svgedit.utilities.getPathBBox = function(path) { - var seglist = path.pathSegList; - var tot = seglist.numberOfItems; - - var bounds = [[], []]; - var start = seglist.getItem(0); - var P0 = [start.x, start.y]; - - for(var i=0; i < tot; i++) { - var seg = seglist.getItem(i); - - if(typeof seg.x == 'undefined') continue; - - // Add actual points to limits - bounds[0].push(P0[0]); - bounds[1].push(P0[1]); - - if(seg.x1) { - var P1 = [seg.x1, seg.y1], - P2 = [seg.x2, seg.y2], - P3 = [seg.x, seg.y]; - - for(var j=0; j < 2; j++) { - - var calc = function(t) { - return Math.pow(1-t,3) * P0[j] - + 3 * Math.pow(1-t,2) * t * P1[j] - + 3 * (1-t) * Math.pow(t,2) * P2[j] - + Math.pow(t,3) * P3[j]; - }; - - var b = 6 * P0[j] - 12 * P1[j] + 6 * P2[j]; - var a = -3 * P0[j] + 9 * P1[j] - 9 * P2[j] + 3 * P3[j]; - var c = 3 * P1[j] - 3 * P0[j]; - - if(a == 0) { - if(b == 0) { - continue; - } - var t = -c / b; - if(0 < t && t < 1) { - bounds[j].push(calc(t)); - } - continue; - } - - var b2ac = Math.pow(b,2) - 4 * c * a; - if(b2ac < 0) continue; - var t1 = (-b + Math.sqrt(b2ac))/(2 * a); - if(0 < t1 && t1 < 1) bounds[j].push(calc(t1)); - var t2 = (-b - Math.sqrt(b2ac))/(2 * a); - if(0 < t2 && t2 < 1) bounds[j].push(calc(t2)); - } - P0 = P3; - } else { - bounds[0].push(seg.x); - bounds[1].push(seg.y); - } - } - - var x = Math.min.apply(null, bounds[0]); - var w = Math.max.apply(null, bounds[0]) - x; - var y = Math.min.apply(null, bounds[1]); - var h = Math.max.apply(null, bounds[1]) - y; - return { - 'x': x, - 'y': y, - 'width': w, - 'height': h - }; -}; - -// Function: groupBBFix -// Get the given/selected element's bounding box object, checking for -// horizontal/vertical lines (see issue 717) -// Note that performance is currently terrible, so some way to improve would -// be great. -// -// Parameters: -// selected - Container or DOM element -function groupBBFix(selected) { - if(svgedit.browser.supportsHVLineContainerBBox()) { - try { return selected.getBBox();} catch(e){} - } - var ref = $.data(selected, 'ref'); - var matched = null; - - if(ref) { - var copy = $(ref).children().clone().attr('visibility', 'hidden'); - $(svgroot_).append(copy); - matched = copy.filter('line, path'); - } else { - matched = $(selected).find('line, path'); - } - - var issue = false; - if(matched.length) { - matched.each(function() { - var bb = this.getBBox(); - if(!bb.width || !bb.height) { - issue = true; - } - }); - if(issue) { - var elems = ref ? copy : $(selected).children(); - ret = getStrokedBBox(elems); - } else { - ret = selected.getBBox(); - } - } else { - ret = selected.getBBox(); - } - if(ref) { - copy.remove(); - } - return ret; -} - -// Function: svgedit.utilities.getBBox -// Get the given/selected element's bounding box object, convert it to be more -// usable when necessary -// -// Parameters: -// elem - Optional DOM element to get the BBox for -svgedit.utilities.getBBox = function(elem) { - var selected = elem || editorContext_.geSelectedElements()[0]; - if (elem.nodeType != 1) return null; - var ret = null; - var elname = selected.nodeName; - - switch ( elname ) { - case 'text': - if(selected.textContent === '') { - selected.textContent = 'a'; // Some character needed for the selector to use. - ret = selected.getBBox(); - selected.textContent = ''; - } else { - try { ret = selected.getBBox();} catch(e){} - } - break; - case 'path': - if(!svgedit.browser.supportsPathBBox()) { - ret = svgedit.utilities.getPathBBox(selected); - } else { - try { ret = selected.getBBox();} catch(e){} - } - break; - case 'g': - case 'a': - ret = groupBBFix(selected); - break; - default: - - if(elname === 'use') { - ret = groupBBFix(selected, true); - } - - if(elname === 'use') { - if(!ret) ret = selected.getBBox(); - // This is resolved in later versions of webkit, perhaps we should - // have a featured detection for correct 'use' behavior? - // —————————— - //if(!svgedit.browser.isWebkit()) { - var bb = {}; - bb.width = ret.width; - bb.height = ret.height; - bb.x = ret.x + parseFloat(selected.getAttribute('x')||0); - bb.y = ret.y + parseFloat(selected.getAttribute('y')||0); - ret = bb; - //} - } else if(~visElems_arr.indexOf(elname)) { - try { ret = selected.getBBox();} - catch(e) { - // Check if element is child of a foreignObject - var fo = $(selected).closest("foreignObject"); - if(fo.length) { - try { - ret = fo[0].getBBox(); - } catch(e) { - ret = null; - } - } else { - ret = null; - } - } - } - } - - if(ret) { - ret = svgedit.utilities.bboxToObj(ret); - } - - // get the bounding box from the DOM (which is in that element's coordinate system) - return ret; -}; - -// Function: svgedit.utilities.getRotationAngle -// Get the rotation angle of the given/selected DOM element -// -// Parameters: -// elem - Optional DOM element to get the angle for -// to_rad - Boolean that when true returns the value in radians rather than degrees -// -// Returns: -// Float with the angle in degrees or radians -svgedit.utilities.getRotationAngle = function(elem, to_rad) { - var selected = elem || editorContext_.getSelectedElements()[0]; - // find the rotation transform (if any) and set it - var tlist = svgedit.transformlist.getTransformList(selected); - if(!tlist) return 0; // elements have no tlist - var N = tlist.numberOfItems; - for (var i = 0; i < N; ++i) { - var xform = tlist.getItem(i); - if (xform.type == 4) { - return to_rad ? xform.angle * Math.PI / 180.0 : xform.angle; - } - } - return 0.0; -}; - -// Function: getElem -// Get a DOM element by ID within the SVG root element. -// -// Parameters: -// id - String with the element's new ID -if (svgedit.browser.supportsSelectors()) { - svgedit.utilities.getElem = function(id) { - // querySelector lookup - return svgroot_.querySelector('#'+id); - }; -} else if (svgedit.browser.supportsXpath()) { - svgedit.utilities.getElem = function(id) { - // xpath lookup - return domdoc_.evaluate( - 'svg:svg[@id="svgroot"]//svg:*[@id="'+id+'"]', - domcontainer_, - function() { return "http://www.w3.org/2000/svg"; }, - 9, - null).singleNodeValue; - }; -} else { - svgedit.utilities.getElem = function(id) { - // jQuery lookup: twice as slow as xpath in FF - return $(svgroot_).find('[id=' + id + ']')[0]; - }; -} - -// Function: assignAttributes -// Assigns multiple attributes to an element. -// -// Parameters: -// node - DOM element to apply new attribute values to -// attrs - Object with attribute keys/values -// suspendLength - Optional integer of milliseconds to suspend redraw -// unitCheck - Boolean to indicate the need to use svgedit.units.setUnitAttr -svgedit.utilities.assignAttributes = function(node, attrs, suspendLength, unitCheck) { - if(!suspendLength) suspendLength = 0; - // Opera has a problem with suspendRedraw() apparently - var handle = null; - if (!svgedit.browser.isOpera()) svgroot_.suspendRedraw(suspendLength); - - for (var i in attrs) { - var ns = (i.substr(0,4) === "xml:" ? XMLNS : - i.substr(0,6) === "xlink:" ? XLINKNS : null); - - if(ns) { - node.setAttributeNS(ns, i, attrs[i]); - } else if(!unitCheck) { - node.setAttribute(i, attrs[i]); - } else { - svgedit.units.setUnitAttr(node, i, attrs[i]); - } - - } - - if (!svgedit.browser.isOpera()) svgroot_.unsuspendRedraw(handle); -}; - -// Function: cleanupElement -// Remove unneeded (default) attributes, makes resulting SVG smaller -// -// Parameters: -// element - DOM element to clean up -svgedit.utilities.cleanupElement = function(element) { - var handle = svgroot_.suspendRedraw(60); - var defaults = { - 'fill-opacity':1, - 'stop-opacity':1, - 'opacity':1, - 'stroke':'none', - 'stroke-dasharray':'none', - 'stroke-linejoin':'miter', - 'stroke-linecap':'butt', - 'stroke-opacity':1, - 'stroke-width':1, - 'rx':0, - 'ry':0 - } - - for(var attr in defaults) { - var val = defaults[attr]; - if(element.getAttribute(attr) == val) { - element.removeAttribute(attr); - } - } - - svgroot_.unsuspendRedraw(handle); -}; - - -})(); diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/touch.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/touch.js deleted file mode 100644 index 3046ff99..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/touch.js +++ /dev/null @@ -1,30 +0,0 @@ -// http://ross.posterous.com/2008/08/19/iphone-touch-events-in-javascript/ - -function touchHandler(event) -{ - - var touches = event.changedTouches, - first = touches[0], - type = ""; - switch(event.type) - { - case "touchstart": type="mousedown"; break; - case "touchmove": type="mousemove"; break; - case "touchend": type="mouseup"; break; - default: return; - } - - //initMouseEvent(type, canBubble, cancelable, view, clickCount, - // screenX, screenY, clientX, clientY, ctrlKey, - // altKey, shiftKey, metaKey, button, relatedTarget); - - var simulatedEvent = document.createEvent("MouseEvent"); - simulatedEvent.initMouseEvent(type, true, true, window, 1, - first.screenX, first.screenY, - first.clientX, first.clientY, false, - false, false, false, 0/*left*/, null); - if(touches.length < 2) { - first.target.dispatchEvent(simulatedEvent); - event.preventDefault(); - } -} diff --git a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/units.js b/image_occlusion_enhanced/svg-edit/svg-edit-2.6/units.js deleted file mode 100644 index d9ec0de2..00000000 --- a/image_occlusion_enhanced/svg-edit/svg-edit-2.6/units.js +++ /dev/null @@ -1,281 +0,0 @@ -/** - * Package: svgedit.units - * - * Licensed under the MIT License - * - * Copyright(c) 2010 Alexis Deveria - * Copyright(c) 2010 Jeff Schiller - */ - -// Dependencies: -// 1) jQuery - -var svgedit = svgedit || {}; - -(function() { - -if (!svgedit.units) { - svgedit.units = {}; -} - -var w_attrs = ['x', 'x1', 'cx', 'rx', 'width']; -var h_attrs = ['y', 'y1', 'cy', 'ry', 'height']; -var unit_attrs = $.merge(['r','radius'], w_attrs); - -var unitNumMap = { - '%': 2, - 'em': 3, - 'ex': 4, - 'px': 5, - 'cm': 6, - 'mm': 7, - 'in': 8, - 'pt': 9, - 'pc': 10 -}; - -$.merge(unit_attrs, h_attrs); - -// Container of elements. -var elementContainer_; - -/** - * Stores mapping of unit type to user coordinates. - */ -var typeMap_ = {px: 1}; - -/** - * ElementContainer interface - * - * function getBaseUnit() - returns a string of the base unit type of the container ("em") - * function getElement() - returns an element in the container given an id - * function getHeight() - returns the container's height - * function getWidth() - returns the container's width - * function getRoundDigits() - returns the number of digits number should be rounded to - */ - -/** - * Function: svgedit.units.init() - * Initializes this module. - * - * Parameters: - * elementContainer - an object implementing the ElementContainer interface. - */ -svgedit.units.init = function(elementContainer) { - elementContainer_ = elementContainer; - - var svgns = 'http://www.w3.org/2000/svg'; - - // Get correct em/ex values by creating a temporary SVG. - var svg = document.createElementNS(svgns, 'svg'); - document.body.appendChild(svg); - var rect = document.createElementNS(svgns,'rect'); - rect.setAttribute('width',"1em"); - rect.setAttribute('height',"1ex"); - rect.setAttribute('x',"1in"); - svg.appendChild(rect); - var bb = rect.getBBox(); - document.body.removeChild(svg); - - var inch = bb.x; - typeMap_['em'] = bb.width; - typeMap_['ex'] = bb.height; - typeMap_['in'] = inch; - typeMap_['cm'] = inch / 2.54; - typeMap_['mm'] = inch / 25.4; - typeMap_['pt'] = inch / 72; - typeMap_['pc'] = inch / 6; - typeMap_['%'] = 0; -}; - -// Group: Unit conversion functions - -// Function: svgedit.units.getTypeMap -// Returns the unit object with values for each unit -svgedit.units.getTypeMap = function() { - return typeMap_; -}; - -// Function: svgedit.units.shortFloat -// Rounds a given value to a float with number of digits defined in save_options -// -// Parameters: -// val - The value as a String, Number or Array of two numbers to be rounded -// -// Returns: -// If a string/number was given, returns a Float. If an array, return a string -// with comma-seperated floats -svgedit.units.shortFloat = function(val) { - var digits = elementContainer_.getRoundDigits(); - if(!isNaN(val)) { - // Note that + converts to Number - return +((+val).toFixed(digits)); - } else if($.isArray(val)) { - return svgedit.units.shortFloat(val[0]) + ',' + svgedit.units.shortFloat(val[1]); - } - return parseFloat(val).toFixed(digits) - 0; -}; - -// Function: svgedit.units.convertUnit -// Converts the number to given unit or baseUnit -svgedit.units.convertUnit = function(val, unit) { - unit = unit || elementContainer_.getBaseUnit(); -// baseVal.convertToSpecifiedUnits(unitNumMap[unit]); -// var val = baseVal.valueInSpecifiedUnits; -// baseVal.convertToSpecifiedUnits(1); - return svgedit.unit.shortFloat(val / typeMap_[unit]); -}; - -// Function: svgedit.units.setUnitAttr -// Sets an element's attribute based on the unit in its current value. -// -// Parameters: -// elem - DOM element to be changed -// attr - String with the name of the attribute associated with the value -// val - String with the attribute value to convert -svgedit.units.setUnitAttr = function(elem, attr, val) { - if(!isNaN(val)) { - // New value is a number, so check currently used unit - var old_val = elem.getAttribute(attr); - - // Enable this for alternate mode -// if(old_val !== null && (isNaN(old_val) || elementContainer_.getBaseUnit() !== 'px')) { -// // Old value was a number, so get unit, then convert -// var unit; -// if(old_val.substr(-1) === '%') { -// var res = getResolution(); -// unit = '%'; -// val *= 100; -// if(w_attrs.indexOf(attr) >= 0) { -// val = val / res.w; -// } else if(h_attrs.indexOf(attr) >= 0) { -// val = val / res.h; -// } else { -// return val / Math.sqrt((res.w*res.w) + (res.h*res.h))/Math.sqrt(2); -// } -// } else { -// if(elementContainer_.getBaseUnit() !== 'px') { -// unit = elementContainer_.getBaseUnit(); -// } else { -// unit = old_val.substr(-2); -// } -// val = val / typeMap_[unit]; -// } -// -// val += unit; -// } - } - elem.setAttribute(attr, val); -}; - -var attrsToConvert = { - "line": ['x1', 'x2', 'y1', 'y2'], - "circle": ['cx', 'cy', 'r'], - "ellipse": ['cx', 'cy', 'rx', 'ry'], - "foreignObject": ['x', 'y', 'width', 'height'], - "rect": ['x', 'y', 'width', 'height'], - "image": ['x', 'y', 'width', 'height'], - "use": ['x', 'y', 'width', 'height'], - "text": ['x', 'y'] -}; - -// Function: svgedit.units.convertAttrs -// Converts all applicable attributes to the configured baseUnit -// -// Parameters: -// element - a DOM element whose attributes should be converted -svgedit.units.convertAttrs = function(element) { - var elName = element.tagName; - var unit = elementContainer_.getBaseUnit(); - var attrs = attrsToConvert[elName]; - if(!attrs) return; - var len = attrs.length - for(var i = 0; i < len; i++) { - var attr = attrs[i]; - var cur = element.getAttribute(attr); - if(cur) { - if(!isNaN(cur)) { - element.setAttribute(attr, (cur / typeMap_[unit]) + unit); - } else { - // Convert existing? - } - } - } -}; - -// Function: svgedit.units.convertToNum -// Converts given values to numbers. Attributes must be supplied in -// case a percentage is given -// -// Parameters: -// attr - String with the name of the attribute associated with the value -// val - String with the attribute value to convert -svgedit.units.convertToNum = function(attr, val) { - // Return a number if that's what it already is - if(!isNaN(val)) return val-0; - - if(val.substr(-1) === '%') { - // Deal with percentage, depends on attribute - var num = val.substr(0, val.length-1)/100; - var width = elementContainer_.getWidth(); - var height = elementContainer_.getHeight(); - - if(w_attrs.indexOf(attr) >= 0) { - return num * width; - } else if(h_attrs.indexOf(attr) >= 0) { - return num * height; - } else { - return num * Math.sqrt((width*width) + (height*height))/Math.sqrt(2); - } - } else { - var unit = val.substr(-2); - var num = val.substr(0, val.length-2); - // Note that this multiplication turns the string into a number - return num * typeMap_[unit]; - } -}; - -// Function: svgedit.units.isValidUnit -// Check if an attribute's value is in a valid format -// -// Parameters: -// attr - String with the name of the attribute associated with the value -// val - String with the attribute value to check -svgedit.units.isValidUnit = function(attr, val, selectedElement) { - var valid = false; - if(unit_attrs.indexOf(attr) >= 0) { - // True if it's just a number - if(!isNaN(val)) { - valid = true; - } else { - // Not a number, check if it has a valid unit - val = val.toLowerCase(); - $.each(typeMap_, function(unit) { - if(valid) return; - var re = new RegExp('^-?[\\d\\.]+' + unit + '$'); - if(re.test(val)) valid = true; - }); - } - } else if (attr == "id") { - // if we're trying to change the id, make sure it's not already present in the doc - // and the id value is valid. - - var result = false; - // because getElem() can throw an exception in the case of an invalid id - // (according to http://www.w3.org/TR/xml-id/ IDs must be a NCName) - // we wrap it in an exception and only return true if the ID was valid and - // not already present - try { - var elem = elementContainer_.getElement(val); - result = (elem == null || elem === selectedElement); - } catch(e) {} - return result; - } else { - valid = true; - } - - return valid; -}; - - -})(); \ No newline at end of file diff --git a/image_occlusion_enhanced/template.py b/image_occlusion_enhanced/template.py deleted file mode 100644 index 9ccbb1ab..00000000 --- a/image_occlusion_enhanced/template.py +++ /dev/null @@ -1,221 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -## Based on Image Occlusion 2.0 ## -## Copyright (c) 2012-2015 tmbb ## -## (https://github.com/tmbb) ## -## ## -#################################################### - -""" -Handles the IO note type and card template -""" - -from config import * - -# Default card template -iocard_front = """\ -{{#%(src_img)s}} -

      {{%(header)s}}
      -
      -
      {{%(que)s}}
      -
      {{%(src_img)s}}
      -
      - -{{/%(src_img)s}} -""" % \ - {'que': IO_FLDS['qm'], - 'ans': IO_FLDS['am'], - 'svg': IO_FLDS['om'], - 'src_img': IO_FLDS['im'], - 'header': IO_FLDS['hd'], - 'footer': IO_FLDS['ft'], - 'remarks': IO_FLDS['rk'], - 'sources': IO_FLDS['sc'], - 'extraone': IO_FLDS['e1'], - 'extratwo': IO_FLDS['e2']} - -iocard_back = """\ -{{#%(src_img)s}} - -
      {{%(header)s}}
      -
      -
      {{%(ans)s}}
      -
      {{%(src_img)s}}
      -
      -{{#%(footer)s}}{{/%(footer)s}} - -
      -
      - {{#%(remarks)s}} -
      -
      %(remarks)s
      {{%(remarks)s}} -
      - {{/%(remarks)s}} - {{#%(sources)s}} -
      -
      %(sources)s
      {{%(sources)s}} -
      - {{/%(sources)s}} - {{#%(extraone)s}} -
      -
      %(extraone)s
      {{%(extraone)s}} -
      - {{/%(extraone)s}} - {{#%(extratwo)s}} -
      -
      %(extratwo)s
      {{%(extratwo)s}} -
      - {{/%(extratwo)s}} -
      -
      -{{/%(src_img)s}} -""" % \ - {'que': IO_FLDS['qm'], - 'ans': IO_FLDS['am'], - 'svg': IO_FLDS['om'], - 'src_img': IO_FLDS['im'], - 'header': IO_FLDS['hd'], - 'footer': IO_FLDS['ft'], - 'remarks': IO_FLDS['rk'], - 'sources': IO_FLDS['sc'], - 'extraone': IO_FLDS['e1'], - 'extratwo': IO_FLDS['e2']} - -iocard_css = """\ -/* GENERAL CARD STYLE */ -.card { - font-family: "Helvetica LT Std", Helvetica, Arial, Sans; - font-size: 150%; - text-align: center; - color: black; - background-color: white; -} - -/* OCCLUSION CSS START - don't edit this */ -#io-overlay { - position:absolute; - top:0; - width:100%; - z-index:3 -} - -#io-original { - position:relative; - top:0; - width:100%; - z-index:2 -} - -#io-wrapper { - position:relative; - width: 100%; -} -/* OCCLUSION CSS END */ - -/* OTHER STYLES */ -#io-header{ - font-size: 1.1em; - margin-bottom: 0.2em; -} - -#io-footer{ - max-width: 80%; - margin-left: auto; - margin-right: auto; - margin-top: 0.8em; - font-style: italic; -} - - -#io-extra-wrapper{ - /* the wrapper is needed to center the - left-aligned blocks below it */ - width: 80%; - margin-left: auto; - margin-right: auto; - margin-top: 0.5em; -} - -#io-extra{ - text-align:center; - display: inline-block; -} - -.io-extra-entry{ - margin-top: 0.8em; - font-size: 0.9em; - text-align:left; -} - -.io-field-descr{ - margin-bottom: 0.2em; - font-weight: bold; - font-size: 1em; -} - -#io-revl-btn { - font-size: 0.5em; -} - -/* ADJUSTMENTS FOR MOBILE DEVICES */ - -.mobile .card, .mobile #content { - font-size: 120%; - margin: 0; -} - -.mobile #io-extra-wrapper { - width: 95%; -} - -.mobile #io-revl-btn { - font-size: 0.8em; -} -""" - -def add_io_model(col): - models = col.models - io_model = models.new(IO_MODEL_NAME) - # Add fields: - for i in IO_FLDS_IDS: - fld = models.newField(IO_FLDS[i]) - if i == "note_id": - fld['size'] = 0 - models.addField(io_model, fld) - # Add template - template = models.newTemplate(IO_CARD_NAME) - template['qfmt'] = iocard_front - template['afmt'] = iocard_back - io_model['css'] = iocard_css - io_model['sortf'] = 1 # set sortfield to header - models.addTemplate(io_model, template) - models.add(io_model) - return io_model - - -def update_template(col): - print "Updating IO Enhanced card template" - io_model = col.models.byName(IO_MODEL_NAME) - # We are assuming that the template list contains only one element. - # This will be true as long as no one has been trampling the model. - template = io_model['tmpls'][0] - template['qfmt'] = iocard_front - template['afmt'] = iocard_back - io_model['css'] = iocard_css - col.models.save() - return io_model diff --git a/image_occlusion_enhanced/utils.py b/image_occlusion_enhanced/utils.py deleted file mode 100644 index 4fa81761..00000000 --- a/image_occlusion_enhanced/utils.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -#################################################### -## ## -## Image Occlusion Enhanced ## -## ## -## Copyright (c) Glutanimate 2016-2017 ## -## (https://github.com/Glutanimate) ## -## ## -#################################################### - -""" -Common reusable utilities -""" - -import os, re - -from aqt import mw - -from xml.dom import minidom -import urlparse, urllib -from imagesize import imagesize - -def path2url(path): - """URL-encode local path""" - return urlparse.urljoin( - 'file:', urllib.pathname2url(path.encode('utf-8'))) - -def fname2img(path): - """Return HTML img element for given path""" - fname = os.path.split(path)[1] - return '' % fname - -def img2path(img, nameonly=False): - """Extract path or file name out of HTML img element""" - imgpatt = r""" 0 - assert height > 0 - except (ValueError, AssertionError): - try: - from Imaging.PIL import Image # fall back to PIL - image = Image.open(image_path) - width, height = image.size - except IOError: - return None, None - return width, height - -def _svg_convert_size(size): - """ - Convert svg size to the px version - :param size: String with the size - """ - - # https://www.w3.org/TR/SVG/coords.html#Units - conversion_table = { - "pt": 1.25, - "pc": 15, - "mm": 3.543307, - "cm": 35.43307, - "in": 90 - } - if len(size) > 3: - if size[-2:] in conversion_table: - return round(float(size[:-2]) * conversion_table[size[-2:]]) - - return round(float(size)) \ No newline at end of file diff --git a/image_occlusion_enhanced/uuid/__init__.py b/image_occlusion_enhanced/uuid/__init__.py deleted file mode 100644 index 7c68785e..00000000 --- a/image_occlusion_enhanced/uuid/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/image_occlusion_enhanced/uuid/uuid.py b/image_occlusion_enhanced/uuid/uuid.py deleted file mode 100644 index 95c1b84c..00000000 --- a/image_occlusion_enhanced/uuid/uuid.py +++ /dev/null @@ -1,577 +0,0 @@ -r"""UUID objects (universally unique identifiers) according to RFC 4122. - -This module provides immutable UUID objects (class UUID) and the functions -uuid1(), uuid3(), uuid4(), uuid5() for generating version 1, 3, 4, and 5 -UUIDs as specified in RFC 4122. - -If all you want is a unique ID, you should probably call uuid1() or uuid4(). -Note that uuid1() may compromise privacy since it creates a UUID containing -the computer's network address. uuid4() creates a random UUID. - -Typical usage: - - >>> import uuid - - # make a UUID based on the host ID and current time - >>> uuid.uuid1() - UUID('a8098c1a-f86e-11da-bd1a-00112444be1e') - - # make a UUID using an MD5 hash of a namespace UUID and a name - >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') - UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e') - - # make a random UUID - >>> uuid.uuid4() - UUID('16fd2706-8baf-433b-82eb-8c7fada847da') - - # make a UUID using a SHA-1 hash of a namespace UUID and a name - >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') - UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d') - - # make a UUID from a string of hex digits (braces and hyphens ignored) - >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') - - # convert a UUID to a string of hex digits in standard form - >>> str(x) - '00010203-0405-0607-0809-0a0b0c0d0e0f' - - # get the raw 16 bytes of the UUID - >>> x.bytes - '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f' - - # make a UUID from a 16-byte string - >>> uuid.UUID(bytes=x.bytes) - UUID('00010203-0405-0607-0809-0a0b0c0d0e0f') -""" - -__author__ = 'Ka-Ping Yee ' - -RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [ - 'reserved for NCS compatibility', 'specified in RFC 4122', - 'reserved for Microsoft compatibility', 'reserved for future definition'] - -class UUID(object): - """Instances of the UUID class represent UUIDs as specified in RFC 4122. - UUID objects are immutable, hashable, and usable as dictionary keys. - Converting a UUID to a string with str() yields something in the form - '12345678-1234-1234-1234-123456789abc'. The UUID constructor accepts - five possible forms: a similar string of hexadecimal digits, or a tuple - of six integer fields (with 32-bit, 16-bit, 16-bit, 8-bit, 8-bit, and - 48-bit values respectively) as an argument named 'fields', or a string - of 16 bytes (with all the integer fields in big-endian order) as an - argument named 'bytes', or a string of 16 bytes (with the first three - fields in little-endian order) as an argument named 'bytes_le', or a - single 128-bit integer as an argument named 'int'. - - UUIDs have these read-only attributes: - - bytes the UUID as a 16-byte string (containing the six - integer fields in big-endian byte order) - - bytes_le the UUID as a 16-byte string (with time_low, time_mid, - and time_hi_version in little-endian byte order) - - fields a tuple of the six integer fields of the UUID, - which are also available as six individual attributes - and two derived attributes: - - time_low the first 32 bits of the UUID - time_mid the next 16 bits of the UUID - time_hi_version the next 16 bits of the UUID - clock_seq_hi_variant the next 8 bits of the UUID - clock_seq_low the next 8 bits of the UUID - node the last 48 bits of the UUID - - time the 60-bit timestamp - clock_seq the 14-bit sequence number - - hex the UUID as a 32-character hexadecimal string - - int the UUID as a 128-bit integer - - urn the UUID as a URN as specified in RFC 4122 - - variant the UUID variant (one of the constants RESERVED_NCS, - RFC_4122, RESERVED_MICROSOFT, or RESERVED_FUTURE) - - version the UUID version number (1 through 5, meaningful only - when the variant is RFC_4122) - """ - - def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, - int=None, version=None): - r"""Create a UUID from either a string of 32 hexadecimal digits, - a string of 16 bytes as the 'bytes' argument, a string of 16 bytes - in little-endian order as the 'bytes_le' argument, a tuple of six - integers (32-bit time_low, 16-bit time_mid, 16-bit time_hi_version, - 8-bit clock_seq_hi_variant, 8-bit clock_seq_low, 48-bit node) as - the 'fields' argument, or a single 128-bit integer as the 'int' - argument. When a string of hex digits is given, curly braces, - hyphens, and a URN prefix are all optional. For example, these - expressions all yield the same UUID: - - UUID('{12345678-1234-5678-1234-567812345678}') - UUID('12345678123456781234567812345678') - UUID('urn:uuid:12345678-1234-5678-1234-567812345678') - UUID(bytes='\x12\x34\x56\x78'*4) - UUID(bytes_le='\x78\x56\x34\x12\x34\x12\x78\x56' + - '\x12\x34\x56\x78\x12\x34\x56\x78') - UUID(fields=(0x12345678, 0x1234, 0x5678, 0x12, 0x34, 0x567812345678)) - UUID(int=0x12345678123456781234567812345678) - - Exactly one of 'hex', 'bytes', 'bytes_le', 'fields', or 'int' must - be given. The 'version' argument is optional; if given, the resulting - UUID will have its variant and version set according to RFC 4122, - overriding the given 'hex', 'bytes', 'bytes_le', 'fields', or 'int'. - """ - - if [hex, bytes, bytes_le, fields, int].count(None) != 4: - raise TypeError('need one of hex, bytes, bytes_le, fields, or int') - if hex is not None: - hex = hex.replace('urn:', '').replace('uuid:', '') - hex = hex.strip('{}').replace('-', '') - if len(hex) != 32: - raise ValueError('badly formed hexadecimal UUID string') - int = long(hex, 16) - if bytes_le is not None: - if len(bytes_le) != 16: - raise ValueError('bytes_le is not a 16-char string') - bytes = (bytes_le[3] + bytes_le[2] + bytes_le[1] + bytes_le[0] + - bytes_le[5] + bytes_le[4] + bytes_le[7] + bytes_le[6] + - bytes_le[8:]) - if bytes is not None: - if len(bytes) != 16: - raise ValueError('bytes is not a 16-char string') - int = long(('%02x'*16) % tuple(map(ord, bytes)), 16) - if fields is not None: - if len(fields) != 6: - raise ValueError('fields is not a 6-tuple') - (time_low, time_mid, time_hi_version, - clock_seq_hi_variant, clock_seq_low, node) = fields - if not 0 <= time_low < 1<<32L: - raise ValueError('field 1 out of range (need a 32-bit value)') - if not 0 <= time_mid < 1<<16L: - raise ValueError('field 2 out of range (need a 16-bit value)') - if not 0 <= time_hi_version < 1<<16L: - raise ValueError('field 3 out of range (need a 16-bit value)') - if not 0 <= clock_seq_hi_variant < 1<<8L: - raise ValueError('field 4 out of range (need an 8-bit value)') - if not 0 <= clock_seq_low < 1<<8L: - raise ValueError('field 5 out of range (need an 8-bit value)') - if not 0 <= node < 1<<48L: - raise ValueError('field 6 out of range (need a 48-bit value)') - clock_seq = (clock_seq_hi_variant << 8L) | clock_seq_low - int = ((time_low << 96L) | (time_mid << 80L) | - (time_hi_version << 64L) | (clock_seq << 48L) | node) - if int is not None: - if not 0 <= int < 1<<128L: - raise ValueError('int is out of range (need a 128-bit value)') - if version is not None: - if not 1 <= version <= 5: - raise ValueError('illegal version number') - # Set the variant to RFC 4122. - int &= ~(0xc000 << 48L) - int |= 0x8000 << 48L - # Set the version number. - int &= ~(0xf000 << 64L) - int |= version << 76L - self.__dict__['int'] = int - - def __cmp__(self, other): - if isinstance(other, UUID): - return cmp(self.int, other.int) - return NotImplemented - - def __hash__(self): - return hash(self.int) - - def __int__(self): - return self.int - - def __repr__(self): - return 'UUID(%r)' % str(self) - - def __setattr__(self, name, value): - raise TypeError('UUID objects are immutable') - - def __str__(self): - hex = '%032x' % self.int - return '%s-%s-%s-%s-%s' % ( - hex[:8], hex[8:12], hex[12:16], hex[16:20], hex[20:]) - - def get_bytes(self): - bytes = '' - for shift in range(0, 128, 8): - bytes = chr((self.int >> shift) & 0xff) + bytes - return bytes - - bytes = property(get_bytes) - - def get_bytes_le(self): - bytes = self.bytes - return (bytes[3] + bytes[2] + bytes[1] + bytes[0] + - bytes[5] + bytes[4] + bytes[7] + bytes[6] + bytes[8:]) - - bytes_le = property(get_bytes_le) - - def get_fields(self): - return (self.time_low, self.time_mid, self.time_hi_version, - self.clock_seq_hi_variant, self.clock_seq_low, self.node) - - fields = property(get_fields) - - def get_time_low(self): - return self.int >> 96L - - time_low = property(get_time_low) - - def get_time_mid(self): - return (self.int >> 80L) & 0xffff - - time_mid = property(get_time_mid) - - def get_time_hi_version(self): - return (self.int >> 64L) & 0xffff - - time_hi_version = property(get_time_hi_version) - - def get_clock_seq_hi_variant(self): - return (self.int >> 56L) & 0xff - - clock_seq_hi_variant = property(get_clock_seq_hi_variant) - - def get_clock_seq_low(self): - return (self.int >> 48L) & 0xff - - clock_seq_low = property(get_clock_seq_low) - - def get_time(self): - return (((self.time_hi_version & 0x0fffL) << 48L) | - (self.time_mid << 32L) | self.time_low) - - time = property(get_time) - - def get_clock_seq(self): - return (((self.clock_seq_hi_variant & 0x3fL) << 8L) | - self.clock_seq_low) - - clock_seq = property(get_clock_seq) - - def get_node(self): - return self.int & 0xffffffffffff - - node = property(get_node) - - def get_hex(self): - return '%032x' % self.int - - hex = property(get_hex) - - def get_urn(self): - return 'urn:uuid:' + str(self) - - urn = property(get_urn) - - def get_variant(self): - if not self.int & (0x8000 << 48L): - return RESERVED_NCS - elif not self.int & (0x4000 << 48L): - return RFC_4122 - elif not self.int & (0x2000 << 48L): - return RESERVED_MICROSOFT - else: - return RESERVED_FUTURE - - variant = property(get_variant) - - def get_version(self): - # The version bits are only meaningful for RFC 4122 UUIDs. - if self.variant == RFC_4122: - return int((self.int >> 76L) & 0xf) - - version = property(get_version) - -def _find_mac(command, args, hw_identifiers, get_index): - import os - path = os.environ.get("PATH", os.defpath).split(os.pathsep) - path.extend(('/sbin', '/usr/sbin')) - for dir in path: - executable = os.path.join(dir, command) - if (os.path.exists(executable) and - os.access(executable, os.F_OK | os.X_OK) and - not os.path.isdir(executable)): - break - else: - return None - - try: - # LC_ALL to ensure English output, 2>/dev/null to - # prevent output on stderr - cmd = 'LC_ALL=C %s %s 2>/dev/null' % (executable, args) - with os.popen(cmd) as pipe: - for line in pipe: - words = line.lower().split() - for i in range(len(words)): - if words[i] in hw_identifiers: - try: - return int( - words[get_index(i)].replace(':', ''), 16) - except (ValueError, IndexError): - # Virtual interfaces, such as those provided by - # VPNs, do not have a colon-delimited MAC address - # as expected, but a 16-byte HWAddr separated by - # dashes. These should be ignored in favor of a - # real MAC address - pass - except IOError: - pass - -def _ifconfig_getnode(): - """Get the hardware address on Unix by running ifconfig.""" - - # This works on Linux ('' or '-a'), Tru64 ('-av'), but not all Unixes. - for args in ('', '-a', '-av'): - mac = _find_mac('ifconfig', args, ['hwaddr', 'ether'], lambda i: i+1) - if mac: - return mac - - import socket - ip_addr = socket.gethostbyname(socket.gethostname()) - - # Try getting the MAC addr from arp based on our IP address (Solaris). - mac = _find_mac('arp', '-an', [ip_addr], lambda i: -1) - if mac: - return mac - - # This might work on HP-UX. - mac = _find_mac('lanscan', '-ai', ['lan0'], lambda i: 0) - if mac: - return mac - - return None - -def _ipconfig_getnode(): - """Get the hardware address on Windows by running ipconfig.exe.""" - import os, re - dirs = ['', r'c:\windows\system32', r'c:\winnt\system32'] - try: - import ctypes - buffer = ctypes.create_string_buffer(300) - ctypes.windll.kernel32.GetSystemDirectoryA(buffer, 300) - dirs.insert(0, buffer.value.decode('mbcs')) - except: - pass - for dir in dirs: - try: - pipe = os.popen(os.path.join(dir, 'ipconfig') + ' /all') - except IOError: - continue - else: - for line in pipe: - value = line.split(':')[-1].strip().lower() - if re.match('([0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value): - return int(value.replace('-', ''), 16) - finally: - pipe.close() - -def _netbios_getnode(): - """Get the hardware address on Windows using NetBIOS calls. - See http://support.microsoft.com/kb/118623 for details.""" - import win32wnet, netbios - ncb = netbios.NCB() - ncb.Command = netbios.NCBENUM - ncb.Buffer = adapters = netbios.LANA_ENUM() - adapters._pack() - if win32wnet.Netbios(ncb) != 0: - return - adapters._unpack() - for i in range(adapters.length): - ncb.Reset() - ncb.Command = netbios.NCBRESET - ncb.Lana_num = ord(adapters.lana[i]) - if win32wnet.Netbios(ncb) != 0: - continue - ncb.Reset() - ncb.Command = netbios.NCBASTAT - ncb.Lana_num = ord(adapters.lana[i]) - ncb.Callname = '*'.ljust(16) - ncb.Buffer = status = netbios.ADAPTER_STATUS() - if win32wnet.Netbios(ncb) != 0: - continue - status._unpack() - bytes = map(ord, status.adapter_address) - return ((bytes[0]<<40L) + (bytes[1]<<32L) + (bytes[2]<<24L) + - (bytes[3]<<16L) + (bytes[4]<<8L) + bytes[5]) - -# Thanks to Thomas Heller for ctypes and for his help with its use here. - -# If ctypes is available, use it to find system routines for UUID generation. -_uuid_generate_random = _uuid_generate_time = _UuidCreate = None -try: - import ctypes, ctypes.util - - # workaround for python bug (https://bugs.python.org/issue17213k) - if os.name not in ['nt', 'ce']: - # The uuid_generate_* routines are provided by libuuid on at least - # Linux and FreeBSD, and provided by libc on Mac OS X. - for libname in ['uuid', 'c']: - try: - lib = ctypes.CDLL(ctypes.util.find_library(libname)) - except: - continue - if hasattr(lib, 'uuid_generate_random'): - _uuid_generate_random = lib.uuid_generate_random - if hasattr(lib, 'uuid_generate_time'): - _uuid_generate_time = lib.uuid_generate_time - if _uuid_generate_random is not None: - break # found everything we were looking for - - # The uuid_generate_* functions are broken on MacOS X 10.5, as noted - # in issue #8621 the function generates the same sequence of values - # in the parent process and all children created using fork (unless - # those children use exec as well). - # - # Assume that the uuid_generate functions are broken from 10.5 onward, - # the test can be adjusted when a later version is fixed. - import sys - if sys.platform == 'darwin': - import os - if int(os.uname()[2].split('.')[0]) >= 9: - _uuid_generate_random = _uuid_generate_time = None - - # On Windows prior to 2000, UuidCreate gives a UUID containing the - # hardware address. On Windows 2000 and later, UuidCreate makes a - # random UUID and UuidCreateSequential gives a UUID containing the - # hardware address. These routines are provided by the RPC runtime. - # NOTE: at least on Tim's WinXP Pro SP2 desktop box, while the last - # 6 bytes returned by UuidCreateSequential are fixed, they don't appear - # to bear any relationship to the MAC address of any network device - # on the box. - try: - lib = ctypes.windll.rpcrt4 - except: - lib = None - _UuidCreate = getattr(lib, 'UuidCreateSequential', - getattr(lib, 'UuidCreate', None)) -except: - pass - -def _unixdll_getnode(): - """Get the hardware address on Unix using ctypes.""" - _buffer = ctypes.create_string_buffer(16) - _uuid_generate_time(_buffer) - return UUID(bytes=_buffer.raw).node - -def _windll_getnode(): - """Get the hardware address on Windows using ctypes.""" - _buffer = ctypes.create_string_buffer(16) - if _UuidCreate(_buffer) == 0: - return UUID(bytes=_buffer.raw).node - -def _random_getnode(): - """Get a random node ID, with eighth bit set as suggested by RFC 4122.""" - import random - return random.randrange(0, 1<<48L) | 0x010000000000L - -_node = None - -def getnode(): - """Get the hardware address as a 48-bit positive integer. - - The first time this runs, it may launch a separate program, which could - be quite slow. If all attempts to obtain the hardware address fail, we - choose a random 48-bit number with its eighth bit set to 1 as recommended - in RFC 4122. - """ - - global _node - if _node is not None: - return _node - - import sys - if sys.platform == 'win32': - getters = [_windll_getnode, _netbios_getnode, _ipconfig_getnode] - else: - getters = [_unixdll_getnode, _ifconfig_getnode] - - for getter in getters + [_random_getnode]: - try: - _node = getter() - except: - continue - if _node is not None: - return _node - -_last_timestamp = None - -def uuid1(node=None, clock_seq=None): - """Generate a UUID from a host ID, sequence number, and the current time. - If 'node' is not given, getnode() is used to obtain the hardware - address. If 'clock_seq' is given, it is used as the sequence number; - otherwise a random 14-bit sequence number is chosen.""" - - # When the system provides a version-1 UUID generator, use it (but don't - # use UuidCreate here because its UUIDs don't conform to RFC 4122). - if _uuid_generate_time and node is clock_seq is None: - _buffer = ctypes.create_string_buffer(16) - _uuid_generate_time(_buffer) - return UUID(bytes=_buffer.raw) - - global _last_timestamp - import time - nanoseconds = int(time.time() * 1e9) - # 0x01b21dd213814000 is the number of 100-ns intervals between the - # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00. - timestamp = int(nanoseconds//100) + 0x01b21dd213814000L - if _last_timestamp is not None and timestamp <= _last_timestamp: - timestamp = _last_timestamp + 1 - _last_timestamp = timestamp - if clock_seq is None: - import random - clock_seq = random.randrange(1<<14L) # instead of stable storage - time_low = timestamp & 0xffffffffL - time_mid = (timestamp >> 32L) & 0xffffL - time_hi_version = (timestamp >> 48L) & 0x0fffL - clock_seq_low = clock_seq & 0xffL - clock_seq_hi_variant = (clock_seq >> 8L) & 0x3fL - if node is None: - node = getnode() - return UUID(fields=(time_low, time_mid, time_hi_version, - clock_seq_hi_variant, clock_seq_low, node), version=1) - -def uuid3(namespace, name): - """Generate a UUID from the MD5 hash of a namespace UUID and a name.""" - from hashlib import md5 - hash = md5(namespace.bytes + name).digest() - return UUID(bytes=hash[:16], version=3) - -def uuid4(): - """Generate a random UUID.""" - - # When the system provides a version-4 UUID generator, use it. - if _uuid_generate_random: - _buffer = ctypes.create_string_buffer(16) - _uuid_generate_random(_buffer) - return UUID(bytes=_buffer.raw) - - # Otherwise, get randomness from urandom or the 'random' module. - try: - import os - return UUID(bytes=os.urandom(16), version=4) - except: - import random - bytes = [chr(random.randrange(256)) for i in range(16)] - return UUID(bytes=bytes, version=4) - -def uuid5(namespace, name): - """Generate a UUID from the SHA-1 hash of a namespace UUID and a name.""" - from hashlib import sha1 - hash = sha1(namespace.bytes + name).digest() - return UUID(bytes=hash[:16], version=5) - -# The following standard UUIDs are for use with uuid3() or uuid5(). - -NAMESPACE_DNS = UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') -NAMESPACE_URL = UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8') -NAMESPACE_OID = UUID('6ba7b812-9dad-11d1-80b4-00c04fd430c8') -NAMESPACE_X500 = UUID('6ba7b814-9dad-11d1-80b4-00c04fd430c8') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..87b10f97 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# Please note that these are only build dependencies. They are not +# required to run compiled builds of the add-on within Anki +aab diff --git a/screenshots/logo.png b/screenshots/logo.png new file mode 100644 index 00000000..fe816a53 Binary files /dev/null and b/screenshots/logo.png differ diff --git a/src/image_occlusion_enhanced/__init__.py b/src/image_occlusion_enhanced/__init__.py new file mode 100644 index 00000000..e40ba58e --- /dev/null +++ b/src/image_occlusion_enhanced/__init__.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + + +from ._version import __version__ # noqa: F401 + +from .libaddon import maybeVendorTyping + +maybeVendorTyping() + + +def initialize_addon(): + """Initializes add-on after performing a few checks + + Allows more fine-grained control over add-on execution, which can + be helpful when implementing workarounds for Anki bugs (e.g. the module + import bug present in all Anki 2.1 versions up to 2.1.14) + """ + + from .libaddon import checkFor2114ImportError + from .consts import ADDON + + if not checkFor2114ImportError(ADDON.NAME): + return False + + from .consts import ADDON + from .libaddon.consts import setAddonProperties + + setAddonProperties(ADDON) + + # from .libaddon.debug import maybeStartDebugging + + # maybeStartDebugging() + + from .main import setup_main + + setup_main() + +initialize_addon() diff --git a/image_occlusion_enhanced/Imaging/__init__.py b/src/image_occlusion_enhanced/_vendor/imagesize/__init__.py similarity index 100% rename from image_occlusion_enhanced/Imaging/__init__.py rename to src/image_occlusion_enhanced/_vendor/imagesize/__init__.py diff --git a/image_occlusion_enhanced/imagesize/imagesize.py b/src/image_occlusion_enhanced/_vendor/imagesize/imagesize.py similarity index 100% rename from image_occlusion_enhanced/imagesize/imagesize.py rename to src/image_occlusion_enhanced/_vendor/imagesize/imagesize.py diff --git a/src/image_occlusion_enhanced/_vendor/imghdr.py b/src/image_occlusion_enhanced/_vendor/imghdr.py new file mode 100644 index 00000000..76e8abb2 --- /dev/null +++ b/src/image_occlusion_enhanced/_vendor/imghdr.py @@ -0,0 +1,168 @@ +"""Recognize image file formats based on their first few bytes.""" + +from os import PathLike + +__all__ = ["what"] + +#-------------------------# +# Recognize image headers # +#-------------------------# + +def what(file, h=None): + f = None + try: + if h is None: + if isinstance(file, (str, PathLike)): + f = open(file, 'rb') + h = f.read(32) + else: + location = file.tell() + h = file.read(32) + file.seek(location) + for tf in tests: + res = tf(h, f) + if res: + return res + finally: + if f: f.close() + return None + + +#---------------------------------# +# Subroutines per image file type # +#---------------------------------# + +tests = [] + +def test_jpeg(h, f): + """JPEG data in JFIF or Exif format""" + if h[6:10] in (b'JFIF', b'Exif'): + return 'jpeg' + +tests.append(test_jpeg) + +def test_png(h, f): + if h.startswith(b'\211PNG\r\n\032\n'): + return 'png' + +tests.append(test_png) + +def test_gif(h, f): + """GIF ('87 and '89 variants)""" + if h[:6] in (b'GIF87a', b'GIF89a'): + return 'gif' + +tests.append(test_gif) + +def test_tiff(h, f): + """TIFF (can be in Motorola or Intel byte order)""" + if h[:2] in (b'MM', b'II'): + return 'tiff' + +tests.append(test_tiff) + +def test_rgb(h, f): + """SGI image library""" + if h.startswith(b'\001\332'): + return 'rgb' + +tests.append(test_rgb) + +def test_pbm(h, f): + """PBM (portable bitmap)""" + if len(h) >= 3 and \ + h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r': + return 'pbm' + +tests.append(test_pbm) + +def test_pgm(h, f): + """PGM (portable graymap)""" + if len(h) >= 3 and \ + h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r': + return 'pgm' + +tests.append(test_pgm) + +def test_ppm(h, f): + """PPM (portable pixmap)""" + if len(h) >= 3 and \ + h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r': + return 'ppm' + +tests.append(test_ppm) + +def test_rast(h, f): + """Sun raster file""" + if h.startswith(b'\x59\xA6\x6A\x95'): + return 'rast' + +tests.append(test_rast) + +def test_xbm(h, f): + """X bitmap (X10 or X11)""" + if h.startswith(b'#define '): + return 'xbm' + +tests.append(test_xbm) + +def test_bmp(h, f): + if h.startswith(b'BM'): + return 'bmp' + +tests.append(test_bmp) + +def test_webp(h, f): + if h.startswith(b'RIFF') and h[8:12] == b'WEBP': + return 'webp' + +tests.append(test_webp) + +def test_exr(h, f): + if h.startswith(b'\x76\x2f\x31\x01'): + return 'exr' + +tests.append(test_exr) + +#--------------------# +# Small test program # +#--------------------# + +def test(): + import sys + recursive = 0 + if sys.argv[1:] and sys.argv[1] == '-r': + del sys.argv[1:2] + recursive = 1 + try: + if sys.argv[1:]: + testall(sys.argv[1:], recursive, 1) + else: + testall(['.'], recursive, 1) + except KeyboardInterrupt: + sys.stderr.write('\n[Interrupted]\n') + sys.exit(1) + +def testall(list, recursive, toplevel): + import sys + import os + for filename in list: + if os.path.isdir(filename): + print(filename + '/:', end=' ') + if recursive or toplevel: + print('recursing down:') + import glob + names = glob.glob(os.path.join(filename, '*')) + testall(names, recursive, 0) + else: + print('*** directory (use -r) ***') + else: + print(filename + ':', end=' ') + sys.stdout.flush() + try: + print(what(filename)) + except OSError: + print('*** not found ***') + +if __name__ == '__main__': + test() diff --git a/src/image_occlusion_enhanced/_version.py b/src/image_occlusion_enhanced/_version.py new file mode 100644 index 00000000..55e8faaf --- /dev/null +++ b/src/image_occlusion_enhanced/_version.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Version information +""" + +__version__ = "v2.0.0-dev.1" diff --git a/src/image_occlusion_enhanced/add.py b/src/image_occlusion_enhanced/add.py new file mode 100644 index 00000000..8f71630f --- /dev/null +++ b/src/image_occlusion_enhanced/add.py @@ -0,0 +1,439 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# Copyright (C) 2012-2015 Tiago Barroso +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Add notes. +""" + +import os +import tempfile + +from aqt.qt import * + +from aqt import mw +from aqt.utils import tooltip, showWarning + +from .ngen import * +from .config import * + +from .editor import ImgOccEdit +from .dialogs import ioCritical, ioInfo +from .utils import imageProp, img2path, path2url +from .lang import _ + +from .consts import SUPPORTED_EXTENSIONS + +# SVG-Edit configuration +svg_edit_dir = os.path.join(os.path.dirname(__file__), "svg-edit", "editor") +svg_edit_path = os.path.join(svg_edit_dir, "svg-editor.html") +svg_edit_ext = "ext-image-occlusion.js,ext-arrows.js,\ +ext-markers.js,ext-shapes.js,ext-eyedropper.js,ext-panning.js,\ +ext-snapping.js" +svg_edit_fonts = "'Helvetica LT Std', Arial, sans-serif" +svg_edit_queryitems = [ + ("initStroke[opacity]", "1"), + ("showRulers", "false"), + ("extensions", svg_edit_ext), +] + + +class ImgOccAdd(object): + def __init__(self, editor, origin, oldimg=None): + self.ed = editor + self.image_path = oldimg + self.mode = "add" + self.origin = origin + self.opref = {} # original io session preference + loadConfig(self) + + def occlude(self, image_path=None): + + note = self.ed.note + isIO = note and note.model() == getOrCreateModel() + + if not image_path: + if self.origin == "addcards": + image_path = self.getNewImage(parent=self.ed.parentWindow) + if not image_path: + return False + elif isIO: + msg, image_path = self.getIONoteData(note) + self.mode = "edit" + if not image_path: + tooltip(msg) + return False + else: + image_path = self.getImageFromFields(note.fields) + if image_path: + tooltip( + _("Non-editable note.
      " "Using image to create new IO note.") + ) + + if not image_path: + tooltip( + ( + _( + "This note cannot be edited, nor is there
      " + "an image to use for an image occlusion." + ) + ) + ) + return False + + self.setPreservedAttrs(note) + self.image_path = image_path + + try: + width, height = imageProp(image_path) + except ValueError as e: + showWarning( + _( + "Unsupported image in file {image_path}:" + "

      {error}" + ).format(image_path=image_path, error=str(e)) + ) + return False + + self.callImgOccEdit(width, height) + + def setPreservedAttrs(self, note): + self.opref["tags"] = self.ed.tags.text() + if self.origin == "addcards": + self.opref["did"] = self.ed.parentWindow.deckChooser.selectedId() + else: + self.opref["did"] = mw.col.db.scalar( + "select did from cards where id = ?", note.cards()[0].id + ) + + def getIONoteData(self, note): + """Select image based on mode and set original field contents""" + + note_id = note[self.ioflds["id"]] + image_path = img2path(note[self.ioflds["im"]]) + omask = img2path(note[self.ioflds["om"]]) + + if note_id is None or note_id.count("-") != 2: + msg = _("Editing unavailable: Invalid image occlusion Note ID") + return msg, None + elif not omask or not image_path: + msg = _("Editing unavailable: Missing image or original mask") + return msg, None + + note_id_grps = note_id.split("-") + self.opref["note_id"] = note_id + self.opref["uniq_id"] = note_id_grps[0] + self.opref["occl_tp"] = note_id_grps[1] + self.opref["image"] = image_path + self.opref["omask"] = omask + + return None, image_path + + def getImageFromFields(self, fields): + """Parse fields for valid images""" + image_path = None + for fld in fields: + image_path = img2path(fld) + if image_path: + break + return image_path + + def getNewImage(self, parent=None, noclip=False): + """Get image from file selection or clipboard""" + if noclip: + clip = None + else: + clip = QApplication.clipboard() + if clip and clip.mimeData().imageData(): + if mw.pm.profile["pastePNG"]: + handle, image_path = tempfile.mkstemp(suffix=".png") + else: + handle, image_path = tempfile.mkstemp(suffix=".jpg") + clip.image().save(image_path) + clip.clear() + if os.stat(image_path).st_size == 0: + # workaround for a clipboard bug + return self.getNewImage(noclip=True) + else: + return str(image_path) + + # retrieve last used image directory + prev_image_dir = self.lconf["dir"] + if not prev_image_dir or not os.path.isdir(prev_image_dir): + prev_image_dir = IO_HOME + + image_path = QFileDialog.getOpenFileName( + parent, + _("Select an Image"), + prev_image_dir, + _("""Image Files ({file_glob_list})""").format( + file_glob_list=" ".join("*." + ext for ext in SUPPORTED_EXTENSIONS) + ), + ) + if image_path: + image_path = image_path[0] + + if not image_path: + return None + elif not os.path.isfile(image_path): + tooltip(_("Invalid image file path")) + return False + else: + self.lconf["dir"] = os.path.dirname(image_path) + return image_path + + def callImgOccEdit(self, width, height): + """Set up variables, call and prepare ImgOccEdit""" + ofill = self.sconf["ofill"] + scol = self.sconf["scol"] + swidth = self.sconf["swidth"] + fsize = self.sconf["fsize"] + font = self.sconf["font"] + + bkgd_url = path2url(self.image_path) + opref = self.opref + onote = self.ed.note + flds = self.mflds + deck = mw.col.decks.nameOrNone(opref["did"]) + + dialog = ImgOccEdit(self, self.ed.parentWindow) + dialog.setupFields(flds) + dialog.switchToMode(self.mode) + self.imgoccedit = dialog + logging.debug(_("Launching new ImgOccEdit instance")) + + url = QUrl.fromLocalFile(svg_edit_path) + # items = QUrlQuery() + # items.setQueryItems(svg_edit_queryitems) + # items.addQueryItem("initFill[color]", ofill) + # items.addQueryItem("dimensions", "{0},{1}".format(width, height)) + # items.addQueryItem("bkgd_url", bkgd_url) + # items.addQueryItem("initStroke[color]", scol) + # items.addQueryItem("initStroke[width]", str(swidth)) + # items.addQueryItem("text[font_size]", str(fsize)) + # items.addQueryItem("text[font_family]", "'%s', %s" % (font, svg_edit_fonts)) + + # if self.mode != "add": + # items.addQueryItem("initTool", "select"), + # for i in flds: + # fn = i["name"] + # if fn in self.ioflds_priv: + # continue + # dialog.tedit[fn].setPlainText(onote[fn].replace("
      ", "\n")) + # svg_url = path2url(opref["omask"]) + # items.addQueryItem("url", svg_url) + # else: + # items.addQueryItem("initTool", "rect"), + + # url.setQuery(items) + dialog.svg_edit.setUrl(url) + dialog.deckChooser.deck.setText(deck) + dialog.tags_edit.setCol(mw.col) + dialog.tags_edit.setText(opref["tags"]) + + if onote: + for i in self.ioflds_prsv: + if i in onote: + dialog.tedit[i].setPlainText(onote[i]) + + if self.mode == "add": + dialog.setModal(False) + + def onSvgEditLoaded(): + dialog.showSvgEdit(True) + dialog.fitImageCanvas() + + else: + # modal dialog when editing + dialog.setModal(True) + + def onSvgEditLoaded(): + # Handle obsolete "aa" occlusion mode: + if self.opref["occl_tp"] == "aa": + ioInfo("obsolete_aa", parent=dialog) + dialog.showSvgEdit(True) + dialog.fitImageCanvas() + + dialog.svg_edit.runOnLoaded(onSvgEditLoaded) + dialog.visible = True + dialog.showSvgEdit(True) + dialog.show() + + def onChangeImage(self): + """Change canvas background image""" + image_path = self.getNewImage() + if not image_path: + return False + try: + width, height = imageProp(image_path) + except ValueError as e: + showWarning( + _( + "Unsupported image in file {image_path}:" + "

      {error}" + ).format(image_path=image_path, error=str(e)) + ) + return False + bkgd_url = path2url(image_path) + self.imgoccedit.svg_edit.eval( + """ + svgCanvas.setBackground('#FFF', '%s'); + svgCanvas.setResolution(%s, %s); + """ + % (bkgd_url, width, height) + ) + self.image_path = image_path + + def onAddNotesButton(self, choice, close): + dialog = self.imgoccedit + # If the user is in in-group editing mode (i.e. editing a shape that + # is grouped with other shapes) svgCanvasToString() doesn't work and + # the callback gets called with `None` (might be a bug in svg-edit). + # Calling leaveContext() first fixes this. + dialog.svg_edit.evalWithCallback( + "svgCanvas.leaveContext(); svgCanvas.svgCanvasToString();", + lambda val, choice=choice, close=close: self._onAddNotesButton( + choice, close, val + ), + ) + + def _onAddNotesButton(self, choice, close, svg): + """Get occlusion settings in and pass them to the note generator (add)""" + dialog = self.imgoccedit + + r1 = self.getUserInputs(dialog) + if r1 is False: + return False + (fields, tags) = r1 + did = dialog.deckChooser.selectedId() + + noteGenerator = genByKey(choice) + gen = noteGenerator( + self.ed, svg, self.image_path, self.opref, tags, fields, did + ) + r = gen.generateNotes() + if r is False: + return False + + if self.origin == "addcards" and self.ed.note: + # Update Editor with modified tags and sources field + self.ed.tags.setText(" ".join(tags)) + self.ed.saveTags() + for i in self.ioflds_prsv: + if i in self.ed.note: + self.ed.note[i] = fields[i] + self.ed.loadNote() + deck = mw.col.decks.nameOrNone(did) + self.ed.parentWindow.deckChooser.deck.setText(deck) + + if close: + dialog.close() + + mw.reset() + + def onEditNotesButton(self, choice): + dialog = self.imgoccedit + # See the comment above in addNotesButton() about + # the call to `leaveContext()`. + dialog.svg_edit.evalWithCallback( + "svgCanvas.leaveContext(); svgCanvas.svgCanvasToString();", + lambda val, choice=choice: self._onEditNotesButton(choice, val), + ) + + def _onEditNotesButton(self, choice, svg): + """Get occlusion settings and pass them to the note generator (edit)""" + dialog = self.imgoccedit + + r1 = self.getUserInputs(dialog, edit=True) + if r1 is False: + return False + (fields, tags) = r1 + did = self.opref["did"] + old_occl_tp = self.opref["occl_tp"] + + noteGenerator = genByKey(choice, old_occl_tp) + gen = noteGenerator( + self.ed, svg, self.image_path, self.opref, tags, fields, did + ) + r = gen.updateNotes() + if r is False: + return False + + if r != "reset": + # no media cache/collection reset required + dialog.close() + + else: + # Refresh image cache + dialog.svg_edit.page().profile().clearHttpCache() + dialog.close() + + # Force EditCurrent and Browser editor instances reload + # in order to make use of refreshed image cache + if not self.origin == "addcards": + + def onToHtmlCallback(html): + if self.ed.web: + self.ed.web.reload() + self.ed.web.setHtml(html) + self.ed.loadNote() + + self.ed.web.page().toHtml(onToHtmlCallback) # async execution + + # write a dummy file to update collection.media modtime and + # force sync + media_dir = mw.col.media.dir() + fpath = os.path.join(media_dir, "syncdummy.txt") + if not os.path.isfile(fpath): + with open(fpath, "w") as f: + f.write("io sync dummy") + os.remove(fpath) + + mw.reset() # FIXME: causes glitches in editcurrent mode + + def getUserInputs(self, dialog, edit=False): + """Get fields and tags from ImgOccEdit while checking note type""" + fields = {} + # note type integrity check: + io_model_fields = mw.col.models.fieldNames(self.model) + if not all(x in io_model_fields for x in list(self.ioflds.values())): + ioCritical("model_error", help="notetype", parent=dialog) + return False + for i in self.mflds: + fn = i["name"] + if fn in self.ioflds_priv: + continue + if edit and fn in self.sconf["skip"]: + continue + text = dialog.tedit[fn].toPlainText().replace("\n", "
      ") + fields[fn] = text + tags = dialog.tags_edit.text().split() + return (fields, tags) diff --git a/src/image_occlusion_enhanced/config.py b/src/image_occlusion_enhanced/config.py new file mode 100644 index 00000000..143cada9 --- /dev/null +++ b/src/image_occlusion_enhanced/config.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# Copyright (C) 2012-2015 Tiago Barroso +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Sets up configuration, including constants +""" + +# TODO: move constants to consts.py + +import os +import sys + +from aqt import mw + +from .lang import _ + +global IO_FLDS, IO_FLDS_IDS +global IO_MODEL_NAME, IO_CARD_NAME, IO_HOME, IO_HOTKEY + +IO_MODEL_NAME = "Image Occlusion Enhanced" +IO_CARD_NAME = "IO Card" + +IO_FLDS = { + "id": "ID (hidden)", + "hd": "Header", + "im": "Image", + "ft": "Footer", + "rk": "Remarks", + "sc": "Sources", + "e1": "Extra 1", + "e2": "Extra 2", + "qm": "Question Mask", + "am": "Answer Mask", + "om": "Original Mask", +} + +IO_FLDS_IDS = ["id", "hd", "im", "qm", "ft", "rk", "sc", "e1", "e2", "am", "om"] + +# TODO: Use IDs instead of names to make these compatible with self.ioflds + +# fields that aren't user-editable +IO_FIDS_PRIV = ["id", "im", "qm", "am", "om"] + +# fields that are synced between an IO Editor session and Anki's Editor +IO_FIDS_PRSV = ["sc"] + +# variables for local preference handling +sys_encoding = sys.getfilesystemencoding() +IO_HOME = os.path.expanduser("~") +IO_HOTKEY = "Ctrl+Shift+O" + +# default configurations +# TODO: update version number before release +default_conf_local = {"version": 1.25, "dir": IO_HOME, "hotkey": IO_HOTKEY} +default_conf_syncd = { + "version": 1.25, + "ofill": "FFEBA2", + "qfill": "FF7E7E", + "scol": "2D2D2D", + "swidth": 3, + "font": "Arial", + "fsize": 24, + "skip": [IO_FLDS["e1"], IO_FLDS["e2"]], + "flds": IO_FLDS, +} + +from . import template + + +def getSyncedConfig(): + # Synced preferences + if "imgocc" not in mw.col.conf: + # create initial configuration + mw.col.conf["imgocc"] = default_conf_syncd + + # upgrade from IO 2.0: + if "image_occlusion_conf" in mw.col.conf: + old_conf = mw.col.conf["image_occlusion_conf"] + mw.col.conf["imgocc"]["ofill"] = old_conf["initFill[color]"] + mw.col.conf["imgocc"]["qfill"] = old_conf["mask_fill_color"] + # insert other upgrade actions here + mw.col.setMod() + + elif mw.col.conf["imgocc"]["version"] < default_conf_syncd["version"]: + print(_("Updating config DB from earlier IO release")) + for key in list(default_conf_syncd.keys()): + if key not in mw.col.conf["imgocc"]: + mw.col.conf["imgocc"][key] = default_conf_syncd[key] + mw.col.conf["imgocc"]["version"] = default_conf_syncd["version"] + mw.col.setMod() + + return mw.col.conf["imgocc"] + + +def getLocalConfig(): + # Local preferences + if "imgocc" not in mw.pm.profile: + mw.pm.profile["imgocc"] = default_conf_local + elif mw.pm.profile["imgocc"].get("version", 0) < default_conf_syncd["version"]: + for key in list(default_conf_local.keys()): + if key not in mw.col.conf["imgocc"]: + mw.pm.profile["imgocc"][key] = default_conf_local[key] + mw.pm.profile["imgocc"]["version"] = default_conf_local["version"] + + return mw.pm.profile["imgocc"] + + +def getOrCreateModel(): + model = mw.col.models.byName(IO_MODEL_NAME) + if not model: + # create model and set up default field name config + model = template.add_io_model(mw.col) + mw.col.conf["imgocc"]["flds"] = default_conf_syncd["flds"] + return model + model_version = mw.col.conf["imgocc"]["version"] + if model_version < default_conf_syncd["version"]: + return template.update_template(mw.col, model_version) + return model + + +def getModelConfig(): + model = getOrCreateModel() + mflds = model["flds"] + ioflds = mw.col.conf["imgocc"]["flds"] + ioflds_priv = [] + for i in IO_FIDS_PRIV: + ioflds_priv.append(ioflds[i]) + # preserve fields if they are marked as sticky in the IO note type: + ioflds_prsv = [] + for fld in mflds: + fname = fld["name"] + if fld["sticky"] and fname not in ioflds_priv: + ioflds_prsv.append(fname) + + return model, mflds, ioflds, ioflds_priv, ioflds_prsv + + +def loadConfig(self): + """load and/or create add-on preferences""" + # FIXME: return config dictionary instead of this hacky + # instantiation of instance variables + self.sconf_dflt = default_conf_syncd + self.lconf_dflt = default_conf_local + self.sconf = getSyncedConfig() + self.lconf = getLocalConfig() + + ( + self.model, + self.mflds, + self.ioflds, + self.ioflds_priv, + self.ioflds_prsv, + ) = getModelConfig() diff --git a/src/image_occlusion_enhanced/consts.py b/src/image_occlusion_enhanced/consts.py new file mode 100644 index 00000000..e1311cd3 --- /dev/null +++ b/src/image_occlusion_enhanced/consts.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Global variables +""" + +import sys +import os + +from ._version import __version__ + +try: + from .data.patrons import MEMBERS_CREDITED, MEMBERS_TOP +except ImportError: + MEMBERS_CREDITED = MEMBERS_TOP = () + +SYS_ENCODING = sys.getfilesystemencoding() + +ADDON_PATH = os.path.dirname(__file__) + +ICONS_PATH = os.path.join(ADDON_PATH, "icons") + +SUPPORTED_BITMAP_FORMATS = ("jpeg", "png", "gif") +SUPPORTED_VECTOR_FORMATS = ("svg",) +SUPPORTED_EXTENSIONS = ("jpg",) + SUPPORTED_BITMAP_FORMATS + SUPPORTED_VECTOR_FORMATS + + +class ADDON(object): + """Class storing general add-on properties + Property names need to be all-uppercase with no leading underscores + """ + + NAME = "Image Occlusion Enhanced" + MODULE = "image_occlusion_enhanced" + ID = "1374772155" + VERSION = __version__ + LICENSE = "GNU AGPLv3" + AUTHORS = ( + { + "name": "Aristotelis P. (Glutanimate)", + "years": "2016-2020", + "contact": "Glutanimate", + }, + {"name": "Tiago Barroso", "years": "2012-1015", "contact": "tmbb@campus.ul.pt"}, + {"name": "Steve AW", "years": "2013", "contact": "https://github.com/steveaw"}, + ) + AUTHOR_MAIL = "ankiglutanimate@gmail.com" + LIBRARIES = () + CONTRIBUTORS = () + SPONSORS = () + MEMBERS_CREDITED = MEMBERS_CREDITED + MEMBERS_TOP = MEMBERS_TOP + LINKS = { + "patreon": "https://www.patreon.com/glutanimate", + "bepatron": "https://www.patreon.com/bePatron?u=7522179", + "coffee": "http://ko-fi.com/glutanimate", + "description": "https://ankiweb.net/shared/info/{}".format(ID), + "rate": "https://ankiweb.net/shared/review/{}".format(ID), + "twitter": "https://twitter.com/glutanimate", + "youtube": "https://www.youtube.com/c/glutanimate", + "help": "", + } diff --git a/src/image_occlusion_enhanced/dialogs.py b/src/image_occlusion_enhanced/dialogs.py new file mode 100644 index 00000000..702ebc51 --- /dev/null +++ b/src/image_occlusion_enhanced/dialogs.py @@ -0,0 +1,309 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# Copyright (C) 2012-2015 Tiago Barroso +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Handles all minor utility dialogs +""" + +from aqt.qt import * +from aqt import mw + +from anki.hooks import addHook, remHook + +from .config import * +from .lang import _ + +# Help and support resource links + +io_link_wiki = "https://github.com/glutanimate/image-occlusion-enhanced/wiki" +io_link_tut = "https://www.youtube.com/playlist?list=PL3MozITKTz5YFHDGB19ypxcYfJ1ITk_6o" +io_link_thread = ( + "https://anki.tenderapp.com/discussions/add-ons/" + "8295-image-occlusion-enhanced-official-thread" +) +io_link_obsolete_aa = ( + "https://github.com/glutanimate/image-occlusion-enhanced/wiki/ADD_LINK_HERE" +) + +# Predefiend dialog messages + +dialog_msg = {} + +dialog_msg["add"] = _( + """ +

      Basic Instructions

      +
        +
      1. With the rectangle tool or any other shape tool selected, cover the areas of the image you want to be tested on
      2. +
      3. (Optional): Fill out additional information about your cards by switching to the Fields tab
      4. +
      5. Click on one of the Add Cards buttons at the bottom of the window to add the cards to your collection
      6. +
      +

      Drawing Custom Labels

      +
        +
      1. Draw up the layers sidepanel by clicking on the Layers button at the right edge of the editor
      2. +
      3. Switch to the Labels layer by left-clicking on it. You can also switch to the labels layer directly by using Ctrl + Shift + L.
      4. +
      5. Anything you draw in this layer – be it text, lines, or shapes – will appear above the image, but still below your masks. All of the painting tools in the left sidebar are at your disposal.
      6. +
      7. Switch back to the masks layer, either via the Layers sidepanel, or by using the Ctrl + Shift + M hotkey.
      8. +
      +

      Grouping Shapes

      +
        +
      1. Select multiple shapes, either by drawing a selection rectangle with the selection tool active (S), or by shift-clicking on multiple shapes
      2. +
      3. Either use the G hotkey or the Group Elements tool in the top-bar to group your items
      4. +
      +

      Grouped shapes will form a single card.

      +

      More Information

      +

      For more information please refer to the following resources:

      + +""" +).format( + io_link_wiki=io_link_wiki, io_link_tut=io_link_tut, io_link_thread=io_link_thread +) + +dialog_msg["edit"] = _( + """ +Instructions for editing: +

      Each mask shape represents a card. +Removing any of the existing shapes will remove the corresponding card. +New shapes will generate new cards. You can change the occlusion type +by using the dropdown box on the left.

      If you click on the +Add new cards button a completely new batch of cards will be +generated, leaving your originals untouched.

      +Actions performed in Image Occlusion's Editing Mode cannot be +easily undone, so please make sure to check your changes twice before +applying them.

      The only exception to this are purely textual +changes to fields like the header or footer of your notes. These can +be fully reverted by using Ctrl+Z in the Browser or Reviewer view.

      +More information: Wiki: Editing Notes. +""" +).format(io_link_wiki=io_link_wiki + "/Basic-Use#editing-cards") + +dialog_msg["notetype"] = _( + """ +Fixing a broken note type: +

      The Image Occlusion Enhanced note type can't be edited +arbitrarily. If you delete a field that's required by the add-on +or rename it outside of the IO Options dialog you will be presented +with an error message.

      To fix this issue please follow the +instructions in the +wiki.""" +).format(io_link_wiki=io_link_wiki + "/Troubleshooting#note-type") + +dialog_msg["main"] = _( + """ +

      Help and Support

      +

      Image Occlusion Enhanced Wiki

      +

      Official Video Tutorial Series

      +

      Support Thread

      +

      Credits and License

      +

      Copyright © 2012-2015 +Tiago Barroso

      +

      Copyright © 2013 +Steve AW

      +

      Copyright © 2016-2017 +Aristotelis P.

      +

      Image Occlusion Enhanced is licensed under the GNU AGPLv3.

      +

      Third-party open-source software shipped with Image Occlusion Enhanced:

      +
      • SVG Edit 2.6. +Copyright (c) 2009-2012 SVG-edit authors. Licensed under the MIT license

      • +
      • Python Imaging Library +(PIL) 1.1.7. Copyright (c) 1997-2011 by Secret Labs AB, Copyright (c) 1995-2011 by Fredrik +Lundh. Licensed under the

      • +
      • imagesize.py v0.7.1. +Copyright (c) 2016 Yoshiki Shibukawa. Licensed under the MIT license.

      • +
      +""" +).format( + io_link_wiki=io_link_wiki, io_link_tut=io_link_tut, io_link_thread=io_link_thread +) + +dialog_msg["obsolete_aa"] = _( + """ +Important

      +The "Hide All, Reveal All" image occlusion mode used by this card +is no longer supported by the add-on. You can still review it just like +you would with any other card, but if you proceed with editing the note, +it will automatically be converted to the "Hide All, Guess One" type.

      +For more information on why this occlusion mode was removed and how to +replicate its functionality please see here:

      +Wiki: Hide All, Reveal All +""" +).format(io_link_obsolete_aa=io_link_obsolete_aa) + +dialog_msg["model_error"] = _( + "Error: Image Occlusion note type " + "not configured properly. Please make sure you did not " + "manually delete or rename any of the default fields." +) + +dialog_msg["question_nconvert"] = _( + """\ +This is a purely experimental feature that is meant to update older +IO notes to be compatible with the new editing feature-set in IO Enhanced. +Clicking on 'Yes' below will prompt the add-on to go through all selected +notes and change their Note ID and mask files in a way that should make it +possible to edit them in the future. +

      Please note that this will only work for notes +that have already been switched to the Image Occlusion Enhanced note type. +If you are coming from IO 2.0 or an older version of IO Enhanced you will +first have to switch the note type of your notes manually by going to Edit → +Change Note Type.

      +WARNING: There is no guarantee that this feature will actually succeed in +updating your notes properly. To convert legacy notes the add-on will have to +make a few assumptions which in some rare instances might turn out to be wrong +and lead to broken notes. Notes that can't be parsed for the information needed +to convert into an editable state (e.g. a valid "Original Mask" field) will usually +be skipped by the add-on, but there might be some corner cases where that won't work. +

      A checkpoint will be set to revert to if needed, +but even with that safety measure in place you should still only use this +function if you know what you are doing. +

      Continue anyway?
      (Depending on the number of notes this might +take a while) +""" +) + +# Message dialog utility functions + + +def ioCritical( + msgkey, title=_("Image Occlusion Enhanced Error"), text="", parent=None, help=None +): + msgfunc = QMessageBox.critical + if help: + buttons = QMessageBox.Help | QMessageBox.Ok + else: + buttons = None + while 1: + r = ioInfo( + msgkey, + title=title, + text=text, + parent=parent, + buttons=buttons, + msgfunc=msgfunc, + ) + if r == QMessageBox.Help: + ioHelp(help, parent=parent) + return False + else: + break + return r + + +def ioAskUser( + msgkey, + title=_("Image Occlusion Enhanced"), + parent=None, + text="", + help="", + defaultno=False, + msgfunc=None, +): + """Show a yes/no question. Return true if yes. + based on askUser by Damien Elmes""" + msgfunc = QMessageBox.question + buttons = QMessageBox.Yes | QMessageBox.No + if help: + buttons |= QMessageBox.Help + while 1: + if defaultno: + default = QMessageBox.No + else: + default = QMessageBox.Yes + r = ioInfo( + msgkey, + title=title, + text=text, + parent=parent, + buttons=buttons, + default=default, + msgfunc=msgfunc, + ) + if r == QMessageBox.Help: + ioHelp(help, parent=parent) + return False + else: + break + return r == QMessageBox.Yes + + +def ioInfo( + msgkey, + title=_("Image Occlusion Enhanced"), + text="", + parent=None, + buttons=None, + default=None, + msgfunc=None, +): + if not parent: + parent = mw.app.activeWindow() + if not buttons: + buttons = QMessageBox.Ok + if not default: + default = QMessageBox.Ok + if not msgfunc: + msgfunc = QMessageBox.information + if msgkey != "custom": + text = dialog_msg[msgkey] + return msgfunc(parent, title, text, buttons, default) + + +def ioHelp(msgkey, title=_("Image Occlusion Enhanced Help"), text="", parent=None): + """Display an info message or a predefined help section""" + if not parent: + parent = mw.app.activeWindow() + if msgkey != "custom": + text = dialog_msg[msgkey] + mbox = QMessageBox(parent) + mbox.setAttribute(Qt.WA_DeleteOnClose) + mbox.setStandardButtons(QMessageBox.Ok) + mbox.setWindowTitle(title) + mbox.setText(text) + mbox.setWindowModality(Qt.NonModal) + + def onProfileUnload(): + if not sip.isdeleted(mbox): + mbox.close() + + try: + from aqt.gui_hooks import profile_will_close + + profile_will_close.append(onProfileUnload) + except (ImportError, ModuleNotFoundError): + addHook("unloadProfile", onProfileUnload) + + mbox.finished.connect(lambda: remHook("unloadProfile", onProfileUnload)) + mbox.show() diff --git a/src/image_occlusion_enhanced/editor.py b/src/image_occlusion_enhanced/editor.py new file mode 100644 index 00000000..36659993 --- /dev/null +++ b/src/image_occlusion_enhanced/editor.py @@ -0,0 +1,492 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# Copyright (C) 2012-2015 Tiago Barroso +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Image Occlusion editor dialog +""" + +import os + +from aqt.qt import * + +from aqt import mw, webview, deckchooser, tagedit, sip +from aqt.utils import saveGeom, restoreGeom +from anki.hooks import addHook, remHook + +from .dialogs import ioHelp +from .consts import * +from .config import * +from .lang import _ + + +class ImgOccWebPage(webview.AnkiWebPage): + def acceptNavigationRequest(self, url, navType, isMainFrame): + return True + + +class ImgOccWebView(webview.AnkiWebView): + def __init__(self, parent=None): + super().__init__(parent=parent) + self._domDone = False + + def _onBridgeCmd(self, cmd): + # ignore webchannel messages that arrive after underlying webview + # deleted + if sip.isdeleted(self): + return + + if cmd == "domDone": + return + + if cmd == "svgEditDone": + self._domDone = True + self._maybeRunActions() + else: + return self.onBridgeCmd(cmd) + + def runOnLoaded(self, callback): + self._domDone = False + self._queueAction("callback", callback) + + def _maybeRunActions(self): + while self._pendingActions and self._domDone: + name, args = self._pendingActions.pop(0) + + if name == "eval": + self._evalWithCallback(*args) + elif name == "setHtml": + self._setHtml(*args) + elif name == "callback": + callback = args[0] + callback() + else: + raise Exception( + _("unknown action: {action_name}").format(action_name=name) + ) + + +class ImgOccEdit(QDialog): + """Main Image Occlusion Editor dialog""" + + def __init__(self, imgoccadd, parent): + QDialog.__init__(self) + mw.setupDialogGC(self) + self.setWindowFlags(Qt.Window) + self.visible = False + self.imgoccadd = imgoccadd + self.parent = parent + self.mode = "add" + loadConfig(self) + self.setupUi() + restoreGeom(self, "imgoccedit") + try: + from aqt.gui_hooks import profile_will_close + + profile_will_close.append(self.onProfileUnload) + except (ImportError, ModuleNotFoundError): + addHook("unloadProfile", self.onProfileUnload) + + def closeEvent(self, event): + if mw.pm.profile is not None: + self.deckChooser.cleanup() + saveGeom(self, "imgoccedit") + self.visible = False + self.svg_edit = None + del self.svg_edit_anim # might not be gc'd + try: + from aqt.gui_hooks import profile_will_close + + profile_will_close.append(self.onProfileUnload) + except (ImportError, ModuleNotFoundError): + remHook("unloadProfile", self.onProfileUnload) + QDialog.reject(self) + + def onProfileUnload(self): + if not sip.isdeleted(self): + self.close() + + def reject(self): + # Override QDialog Esc key reject + pass + + def setupUi(self): + """Set up ImgOccEdit UI""" + # Main widgets aside from fields + self.svg_edit = ImgOccWebView(parent=self) + self.svg_edit._page = ImgOccWebPage(self.svg_edit._onBridgeCmd) + self.svg_edit.setPage(self.svg_edit._page) + + self.tags_hbox = QHBoxLayout() + self.tags_edit = tagedit.TagEdit(self) + self.tags_label = QLabel(_("Tags")) + self.tags_label.setFixedWidth(70) + self.deck_container = QWidget() + self.deckChooser = deckchooser.DeckChooser(mw, self.deck_container, label=True) + self.deckChooser.deck.setAutoDefault(False) + + # workaround for tab focus order issue of the tags entry + # (this particular section is only needed when the quick deck + # buttons add-on is installed) + if self.deck_container.layout().children(): # multiple deck buttons + for i in range(self.deck_container.layout().children()[0].count()): + try: + item = self.deck_container.layout().children()[0].itemAt(i) + # remove Tab focus manually: + item.widget().setFocusPolicy(Qt.ClickFocus) + item.widget().setAutoDefault(False) + except AttributeError: + pass + + # Button row widgets + self.bottom_label = QLabel() + button_box = QDialogButtonBox(Qt.Horizontal, self) + button_box.setCenterButtons(False) + + image_btn = QPushButton(_("Change &Image"), clicked=self.changeImage) + image_btn.setIcon(QIcon(os.path.join(ICONS_PATH, "add.png"))) + image_btn.setIconSize(QSize(16, 16)) + image_btn.setAutoDefault(False) + help_btn = QPushButton(_("&Help"), clicked=self.onHelp) + help_btn.setAutoDefault(False) + + self.occl_tp_select = QComboBox() + self.occl_tp_select.addItem(_("Don't Change"), "Don't Change") + self.occl_tp_select.addItem(_("Hide All, Guess One"), "Hide All, Guess One") + self.occl_tp_select.addItem(_("Hide One, Guess One"), "Hide One, Guess One") + + self.edit_btn = button_box.addButton( + _("&Edit Cards"), QDialogButtonBox.ActionRole + ) + self.new_btn = button_box.addButton( + _("&Add New Cards"), QDialogButtonBox.ActionRole + ) + self.ao_btn = button_box.addButton( + _("Hide &All, Guess One"), QDialogButtonBox.ActionRole + ) + self.oa_btn = button_box.addButton( + _("Hide &One, Guess One"), QDialogButtonBox.ActionRole + ) + close_button = button_box.addButton(_("&Close"), QDialogButtonBox.RejectRole) + + image_tt = _( + "Switch to a different image while preserving all of " + "the shapes and fields" + ) + dc_tt = _("Preserve existing occlusion type") + edit_tt = _("Edit all cards using current mask shapes and field " "entries") + new_tt = _("Create new batch of cards without editing existing ones") + ao_tt = _( + "Generate cards with nonoverlapping information, where all" + "
      labels are hidden on the front and one revealed on the" + " back" + ) + oa_tt = _( + "Generate cards with overlapping information, where one
      " + "label is hidden on the front and revealed on the back" + ) + close_tt = _("Close Image Occlusion Editor without generating cards") + + image_btn.setToolTip(image_tt) + self.edit_btn.setToolTip(edit_tt) + self.new_btn.setToolTip(new_tt) + self.ao_btn.setToolTip(ao_tt) + self.oa_btn.setToolTip(oa_tt) + close_button.setToolTip(close_tt) + self.occl_tp_select.setItemData(0, dc_tt, Qt.ToolTipRole) + self.occl_tp_select.setItemData(1, ao_tt, Qt.ToolTipRole) + self.occl_tp_select.setItemData(2, oa_tt, Qt.ToolTipRole) + + for btn in [ + image_btn, + self.edit_btn, + self.new_btn, + self.ao_btn, + self.oa_btn, + close_button, + ]: + btn.setFocusPolicy(Qt.ClickFocus) + + self.edit_btn.clicked.connect(self.editNote) + self.new_btn.clicked.connect(self.new) + self.ao_btn.clicked.connect(self.addAO) + self.oa_btn.clicked.connect(self.addOA) + close_button.clicked.connect(self.close) + + # Set basic layout up + + # Button row + bottom_hbox = QHBoxLayout() + bottom_hbox.addWidget(image_btn) + bottom_hbox.addWidget(help_btn) + bottom_hbox.insertStretch(2, stretch=1) + bottom_hbox.addWidget(self.bottom_label) + bottom_hbox.addWidget(self.occl_tp_select) + bottom_hbox.addWidget(button_box) + + # Tab 1 + vbox1 = QVBoxLayout() + + svg_edit_loader = QLabel(_("Loading...")) + svg_edit_loader.setAlignment(Qt.AlignCenter) + loader_icon = os.path.join(ICONS_PATH, "loader.gif") + anim = QMovie(loader_icon) + svg_edit_loader.setMovie(anim) + anim.start() + self.svg_edit_loader = svg_edit_loader + self.svg_edit_anim = anim + + vbox1.addWidget(self.svg_edit, stretch=1) + vbox1.addWidget(self.svg_edit_loader, stretch=1) + + # Tab 2 + # vbox2 fields are variable and added by setupFields() at a later point + self.vbox2 = QVBoxLayout() + + # Main Tab Widget + tab1 = QWidget() + self.tab2 = QWidget() + tab1.setLayout(vbox1) + self.tab2.setLayout(self.vbox2) + self.tab_widget = QTabWidget() + self.tab_widget.setFocusPolicy(Qt.ClickFocus) + self.tab_widget.addTab(tab1, _("&Masks Editor")) + self.tab_widget.addTab(self.tab2, _("&Fields")) + self.tab_widget.setTabToolTip(1, _("Include additional information (optional)")) + self.tab_widget.setTabToolTip(0, _("Create image occlusion masks (required)")) + + # Main Window + vbox_main = QVBoxLayout() + vbox_main.addWidget(self.tab_widget) + vbox_main.addLayout(bottom_hbox) + self.setLayout(vbox_main) + self.setMinimumWidth(640) + self.tab_widget.setCurrentIndex(0) + self.svg_edit.setFocus() + self.showSvgEdit(False) + + # Define and connect key bindings + + # Field focus hotkeys + for i in range(1, 10): + QShortcut(QKeySequence("Ctrl+%i" % i), self).activated.connect( + lambda f=i - 1: self.focusField(f) + ) + # Other hotkeys + QShortcut(QKeySequence("Ctrl+Return"), self).activated.connect( + lambda: self.defaultAction(True) + ) + QShortcut(QKeySequence("Ctrl+Shift+Return"), self).activated.connect( + lambda: self.addOA(True) + ) + QShortcut(QKeySequence("Ctrl+Tab"), self).activated.connect(self.switchTabs) + QShortcut(QKeySequence("Ctrl+r"), self).activated.connect(self.resetMainFields) + QShortcut(QKeySequence("Ctrl+Shift+r"), self).activated.connect( + self.resetAllFields + ) + QShortcut(QKeySequence("Ctrl+Shift+t"), self).activated.connect(self.focusTags) + QShortcut(QKeySequence("Ctrl+f"), self).activated.connect(self.fitImageCanvas) + + # Various actions that act on / interact with the ImgOccEdit UI: + + # Note actions + + def changeImage(self): + self.imgoccadd.onChangeImage() + self.fitImageCanvas() + + def defaultAction(self, close): + if self.mode == "add": + self.addAO(close) + else: + self.editNote() + + def addAO(self, close=False): + self.imgoccadd.onAddNotesButton("ao", close) + + def addOA(self, close=False): + self.imgoccadd.onAddNotesButton("oa", close) + + def new(self, close=False): + choice = self.occl_tp_select.currentData() + self.imgoccadd.onAddNotesButton(choice, close) + + def editNote(self): + choice = self.occl_tp_select.currentData() + self.imgoccadd.onEditNotesButton(choice) + + def onHelp(self): + if self.mode == "add": + ioHelp("add", parent=self) + else: + ioHelp("edit", parent=self) + + # Window state + + def resetFields(self): + """Reset all widgets. Needed for changes to the note type""" + layout = self.vbox2 + for i in reversed(list(range(layout.count()))): + item = layout.takeAt(i) + layout.removeItem(item) + if item.widget(): + item.widget().setParent(None) + elif item.layout(): + sublayout = item.layout() + sublayout.setParent(None) + for i in reversed(list(range(sublayout.count()))): + subitem = sublayout.takeAt(i) + sublayout.removeItem(subitem) + subitem.widget().setParent(None) + self.tags_hbox.setParent(None) + + def setupFields(self, flds): + """Setup dialog text edits based on note type fields""" + self.tedit = {} + self.tlabel = {} + self.flds = flds + for i in flds: + if i["name"] in self.ioflds_priv: + continue + hbox = QHBoxLayout() + tedit = QPlainTextEdit() + label = QLabel(i["name"]) + hbox.addWidget(label) + hbox.addWidget(tedit) + tedit.setTabChangesFocus(True) + tedit.setMinimumHeight(40) + label.setFixedWidth(70) + self.tedit[i["name"]] = tedit + self.tlabel[i["name"]] = label + self.vbox2.addLayout(hbox) + + self.tags_hbox.addWidget(self.tags_label) + self.tags_hbox.addWidget(self.tags_edit) + self.vbox2.addLayout(self.tags_hbox) + self.vbox2.addWidget(self.deck_container) + # switch Tab focus order of deckchooser and tags_edit ( + # for some reason it's the wrong way around by default): + self.tab2.setTabOrder(self.tags_edit, self.deckChooser.deck) + + def switchToMode(self, mode): + """Toggle between add and edit layouts""" + hide_on_add = [self.occl_tp_select, self.edit_btn, self.new_btn] + hide_on_edit = [self.ao_btn, self.oa_btn] + self.mode = mode + for i in list(self.tedit.values()): + i.show() + for i in list(self.tlabel.values()): + i.show() + if mode == "add": + for i in hide_on_add: + i.hide() + for i in hide_on_edit: + i.show() + dl_txt = _("Deck") + ttl = _("Image Occlusion Enhanced - Add Mode") + bl_txt = _("Add Cards:") + else: + for i in hide_on_add: + i.show() + for i in hide_on_edit: + i.hide() + for i in self.sconf["skip"]: + if i in list(self.tedit.keys()): + self.tedit[i].hide() + self.tlabel[i].hide() + dl_txt = _("Deck for Add new cards") + ttl = _("Image Occlusion Enhanced - Editing Mode") + bl_txt = _("Type:") + self.deckChooser.deckLabel.setText(dl_txt) + self.setWindowTitle(ttl) + self.bottom_label.setText(bl_txt) + + def showSvgEdit(self, state): + if not state: + self.svg_edit.hide() + self.svg_edit_anim.start() + self.svg_edit_loader.show() + else: + self.svg_edit_anim.stop() + self.svg_edit_loader.hide() + self.svg_edit.show() + + # Other actions + + def switchTabs(self): + currentTab = self.tab_widget.currentIndex() + if currentTab == 0: + self.tab_widget.setCurrentIndex(1) + if isinstance(QApplication.focusWidget(), QPushButton): + self.tedit[self.ioflds["hd"]].setFocus() + else: + self.tab_widget.setCurrentIndex(0) + + def focusField(self, idx): + """Focus field in vbox2 layout by index number""" + self.tab_widget.setCurrentIndex(1) + target_item = self.vbox2.itemAt(idx) + if not target_item: + return + target_layout = target_item.layout() + target_widget = target_item.widget() + if target_layout: + target = target_layout.itemAt(1).widget() + elif target_widget: + target = target_widget + target.setFocus() + + def focusTags(self): + self.tab_widget.setCurrentIndex(1) + self.tags_edit.setFocus() + + def resetMainFields(self): + """Reset all fields aside from sticky ones""" + for i in self.flds: + fn = i["name"] + if fn in self.ioflds_priv or fn in self.ioflds_prsv: + continue + self.tedit[fn].setPlainText("") + + def resetAllFields(self): + """Reset all fields""" + self.resetMainFields() + for i in self.ioflds_prsv: + self.tedit[i].setPlainText("") + + def fitImageCanvas(self): + self.svg_edit.eval( + """ + setTimeout(function(){ + svgCanvas.zoomChanged('', 'canvas'); + }, 5) + """ + ) diff --git a/image_occlusion_enhanced/icons/new_occlusion.png b/src/image_occlusion_enhanced/icons/add.png similarity index 100% rename from image_occlusion_enhanced/icons/new_occlusion.png rename to src/image_occlusion_enhanced/icons/add.png diff --git a/image_occlusion_enhanced/icons/edit_occlusion.png b/src/image_occlusion_enhanced/icons/edit.png similarity index 100% rename from image_occlusion_enhanced/icons/edit_occlusion.png rename to src/image_occlusion_enhanced/icons/edit.png diff --git a/src/image_occlusion_enhanced/icons/loader.gif b/src/image_occlusion_enhanced/icons/loader.gif new file mode 100644 index 00000000..1c72ebb5 Binary files /dev/null and b/src/image_occlusion_enhanced/icons/loader.gif differ diff --git a/src/image_occlusion_enhanced/lang.py b/src/image_occlusion_enhanced/lang.py new file mode 100644 index 00000000..c4679b12 --- /dev/null +++ b/src/image_occlusion_enhanced/lang.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +#################################################### +## ## +## Image Occlusion Enhanced ## +## ## +## Copyright (c) Glutanimate 2016-2019 ## +## (https://github.com/Glutanimate) ## +## ## +## Based on Image Occlusion 2.0 ## +## Copyright (c) 2012-2015 tmbb ## +## (https://github.com/tmbb) ## +## ## +#################################################### + +# This source code file takes most of its contents from +# Anki's original 'anki.lang' module with the following +# changes: +# - change location of language files and remove os specific handling +# - get the initialization language from Anki's currently set language +# - use Anki's code and variables whenever possible +# - remove unneeded functions and variables +# +# Modifications were made on 2020-02-23. +# Link to original file at time of creation: +# https://github.com/dae/anki/blob/241b7ea005e2360ea8c1e0a1dd91d8b4dda4bf0e/anki/lang.py + +""" +Handle translation. +""" + +import os +import gettext + +import anki.lang +from aqt import mw + +# TODO maybe remove the table and 'mungeCode' once Anki 2.1.16 is released and +# replace with calls to the API to reduce copied code +# compatibility with old versions +compatMap = { + "af": "af_ZA", + "ar": "ar_SA", + "bg": "bg_BG", + "ca": "ca_ES", + "cs": "cs_CZ", + "da": "da_DK", + "de": "de_DE", + "el": "el_GR", + "en": "en_US", + "eo": "eo_UY", + "es": "es_ES", + "et": "et_EE", + "eu": "eu_ES", + "fa": "fa_IR", + "fi": "fi_FI", + "fr": "fr_FR", + "gl": "gl_ES", + "he": "he_IL", + "hr": "hr_HR", + "hu": "hu_HU", + "hy": "hy_AM", + "it": "it_IT", + "ja": "ja_JP", + "ko": "ko_KR", + "mn": "mn_MN", + "ms": "ms_MY", + "nl": "nl_NL", + "nb": "nb_NL", + "no": "nb_NL", + "oc": "oc_FR", + "pl": "pl_PL", + "pt": "pt_PT", + "ro": "ro_RO", + "ru": "ru_RU", + "sk": "sk_SK", + "sl": "sl_SI", + "sr": "sr_SP", + "sv": "sv_SE", + "th": "th_TH", + "tr": "tr_TR", + "uk": "uk_UA", + "vi": "vi_VN", +} + +# global defaults +currentLang = None +currentTranslation = None + + +def localTranslation(): + "Return the translation local to this thread, or the default." + return currentTranslation + + +def _(str): + return localTranslation().gettext(str) + + +def ngettext(single, plural, n): + return localTranslation().ngettext(single, plural, n) + + +def langDir(): + filedir = os.path.dirname(os.path.abspath(__file__)) + return os.path.abspath(os.path.join(filedir, "locale")) + + +def setLang(lang): + lang = mungeCode(lang) + trans = gettext.translation( + "anki-image-occlusion-enhanced", langDir(), languages=[lang], fallback=True + ) + global currentLang, currentTranslation + currentLang = lang + currentTranslation = trans + + +def mungeCode(code): + code = code.replace("-", "_") + if code in compatMap: + code = compatMap[code] + + return code + + +if not currentTranslation: + if hasattr(anki.lang, "getLang"): + # backwards compatibility Anki < 2.1.22 + # (commit 6c9e9eb3300707f534af5b57e45a0cec7ed91401) + setLang(anki.lang.getLang()) + else: + setLang(anki.lang.currentLang or mw.pm.meta["defaultLang"] or "en_US") diff --git a/src/image_occlusion_enhanced/libaddon/LICENSE b/src/image_occlusion_enhanced/libaddon/LICENSE new file mode 100644 index 00000000..017324e8 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/LICENSE @@ -0,0 +1,713 @@ +This Program is licensed under the GNU Affero General Public License +version 3 ("AGPL"), extended by a number of Additional Terms under +Section 7 of the AGPL. + +If not otherwise noted, this License applies to all files included +with this Program. Files subject to different licensing terms might +also ship with this Program, but will be clearly marked as such in +additional LICENSE files accompanying them. + +The AGPLv3 License and Additional Terms follow. + +============================================================================== + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + +=============================================================================== + + ADDITIONAL TERMS APPLICABLE TO THIS PROGRAM + UNDER GNU AGPL VERSION 3 SECTION 7 + +The following additional terms ("Additional Terms") supplement and modify the +GNU Affero General Public License, Version 3 ("AGPL") applicable to the present +Program. + +In addition to the terms and conditions of the AGPL, the present Program is +subject to the further restrictions below: + +1. Trademark and Publicity Rights. + + Except as expressly provided herein, no trademark or publicity rights are + granted. This license does NOT give you any right, title or interest in the + "Glutanimate" name or logo. + + However, Licensees are granted a limited, non-exclusive right to use the + name "Glutanimate" and the Glutanimate logo as part of the preservation + of the legal notices and author attributions within this Program and + within Appropriate Legal Notices displayed by works containing it. + +2. Origin of the Program. + + The origin of the Program must not be misrepresented; you must not claim + that you wrote the original Program. Altered source versions must be plainly + marked as such, and must not be misrepresented as being the original + Program. + +3. Legal Notices and Author Attributions. + + You must reproduce faithfully all trademark, copyright and other proprietary + and legal notices on any copies of the Program or any other required author + attributions. Legal notices or author attributions displayed as part of the + user interface must be preserved as such. + + Additionally, all copies of any covered work conveyed by you must include + in the Appropriate Legal Notices the following author attribution: + + "Uses Libaddon by Glutanimate. Click here to support Glutanimate's work." + + The word "Libaddon" must be a clickable hyperlink that leads directly + to the Internet URL "https://github.com/glutanimate/anki-libaddon/". + The words "Click here to support Glutanimate's work." must be a + clickable hyperlink that leads directly to the Internet URL + "https://glutanimate.com/support-my-work/". + + This attribution must also be included in copyright notices in the + README or description of any covered work conveyed by you (e.g. add-on + description text on AnkiWeb or README on GitHub). + +4. Use of Names of Licensors or Authors for Publicity Purposes. + + Outside of the aforementioned legal notices and author attributions, neither + the name of the copyright holder or its affiliates, any other party who + modifies and/or conveys the Program, nor the names of the Program's + sponsors/supporters/patrons may be used to endorse or promote products + derived from this software without specific prior written permission. + +5. Indemnification. + + IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT OF THAT COVERED + WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK, YOU HEREBY + AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS AND AUTHORS + OF THAT COVERED WORK FOR ANY DAMAGES, DEMANDS, CLAIMS, LOSSES, CAUSES OF + ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION + REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABLITY ARISING FROM, + RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. + +6. Preservation of Licensing Terms. + + Any covered work conveyed by you must include this license text in its + entirety. + +------------------------------------------------------------------------------- + +If you have any questions regarding this license, about any other legal +details, or want to report an infringement of the aforementioned licensing +terms, please feel free to contact me at: diff --git a/src/image_occlusion_enhanced/libaddon/__init__.py b/src/image_occlusion_enhanced/libaddon/__init__.py new file mode 100644 index 00000000..df6aa46a --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/__init__.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Libaddon: A helper library for Anki add-on development + +Provides access to a number of commonly used modules shared across +many of my add-ons. + +Please note that this package is not fit for general use yet, as it is +still is too specific to my own add-ons and implementations. + +This module is the package entry-point. +""" + +from ._version import __version__ # noqa: F401 + + +def maybeVendorTyping(): + try: + import typing # noqa: F401 + import types # noqa: F401 + except ImportError: + registerLegacyVendorDir() + + +def registerLegacyVendorDir(): + """Some modules like "typing" cannot be properly vendorized, so fall back + to hacky sys.path modifications if necessary + NOTE: make sure not to use vendored legacy dependencies before running this + """ + import sys + import os + + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_vendor_legacy")) + + +def checkFor2114ImportError(name: str) -> bool: + try: + # litmus test for Anki import bug + from .platform import anki_version # noqa: F401 + + return True + except ImportError: + # Disable add-on and inform user of the bug + from aqt.utils import showWarning + from aqt import mw + from anki import version as anki_version + + if mw is None: + return False + + mw.addonManager.toggleEnabled(__name__, enable=False) + + bug = "https://anki.tenderapp.com/discussions/ankidesktop/34836" + downloads = "https://apps.ankiweb.net#download" + vers = "2.1.15" + title = "Warning: {name} disabled".format(name=name) + msg = ( + "WARNING: {name} had to be disabled because the " + "version of Anki that is currently installed on your system " + "({anki_version}) is incompatible with the add-on.

      " + "Earlier releases of Anki like this one " + "suffer from a bug that breaks " + "{name} and many other add-ons on your system. " + "In order to fix this you will have to update Anki " + "to version {vers} or higher.

      " + "After updating Anki, please re-enable " + "{name} by heading to Tools → Add-ons, selecting the " + "add-on, and clicking Toggle Enabled.".format( + name=name, + anki_version=anki_version, + bug=bug, + vers=vers, + downloads=downloads, + ) + ) + + showWarning(msg, title=title, textFormat="rich") + + return False diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/PYTHON.LICENSE b/src/image_occlusion_enhanced/libaddon/_vendor/PYTHON.LICENSE new file mode 100644 index 00000000..5c201804 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/PYTHON.LICENSE @@ -0,0 +1,260 @@ +This license pertains to the following Python standard library packages: + +types typing logging + +----------------------------- LICENSE TEXT ------------------------------- + +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation; All +Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/__init__.py b/src/image_occlusion_enhanced/libaddon/_vendor/__init__.py new file mode 100644 index 00000000..94bf4a30 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Vendorized third-party packages +""" diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/logging/__init__.py b/src/image_occlusion_enhanced/libaddon/_vendor/logging/__init__.py new file mode 100644 index 00000000..386ed177 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/logging/__init__.py @@ -0,0 +1,2021 @@ +# Copyright 2001-2016 by Vinay Sajip. All Rights Reserved. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose and without fee is hereby granted, +# provided that the above copyright notice appear in all copies and that +# both that copyright notice and this permission notice appear in +# supporting documentation, and that the name of Vinay Sajip +# not be used in advertising or publicity pertaining to distribution +# of the software without specific, written prior permission. +# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +""" +Logging package for Python. Based on PEP 282 and comments thereto in +comp.lang.python. + +Copyright (C) 2001-2016 Vinay Sajip. All Rights Reserved. + +To use, simply 'import logging' and log away! +""" + +import sys, os, time, io, traceback, warnings, weakref, collections + +from string import Template + +__all__ = ['BASIC_FORMAT', 'BufferingFormatter', 'CRITICAL', 'DEBUG', 'ERROR', + 'FATAL', 'FileHandler', 'Filter', 'Formatter', 'Handler', 'INFO', + 'LogRecord', 'Logger', 'LoggerAdapter', 'NOTSET', 'NullHandler', + 'StreamHandler', 'WARN', 'WARNING', 'addLevelName', 'basicConfig', + 'captureWarnings', 'critical', 'debug', 'disable', 'error', + 'exception', 'fatal', 'getLevelName', 'getLogger', 'getLoggerClass', + 'info', 'log', 'makeLogRecord', 'setLoggerClass', 'shutdown', + 'warn', 'warning', 'getLogRecordFactory', 'setLogRecordFactory', + 'lastResort', 'raiseExceptions'] + +try: + import threading +except ImportError: #pragma: no cover + threading = None + +__author__ = "Vinay Sajip " +__status__ = "production" +# The following module attributes are no longer updated. +__version__ = "0.5.1.2" +__date__ = "07 February 2010" + +#--------------------------------------------------------------------------- +# Miscellaneous module data +#--------------------------------------------------------------------------- + +# +#_startTime is used as the base when calculating the relative time of events +# +_startTime = time.time() + +# +#raiseExceptions is used to see if exceptions during handling should be +#propagated +# +raiseExceptions = True + +# +# If you don't want threading information in the log, set this to zero +# +logThreads = True + +# +# If you don't want multiprocessing information in the log, set this to zero +# +logMultiprocessing = True + +# +# If you don't want process information in the log, set this to zero +# +logProcesses = True + +#--------------------------------------------------------------------------- +# Level related stuff +#--------------------------------------------------------------------------- +# +# Default levels and level names, these can be replaced with any positive set +# of values having corresponding names. There is a pseudo-level, NOTSET, which +# is only really there as a lower limit for user-defined levels. Handlers and +# loggers are initialized with NOTSET so that they will log all messages, even +# at user-defined levels. +# + +CRITICAL = 50 +FATAL = CRITICAL +ERROR = 40 +WARNING = 30 +WARN = WARNING +INFO = 20 +DEBUG = 10 +NOTSET = 0 + +_levelToName = { + CRITICAL: 'CRITICAL', + ERROR: 'ERROR', + WARNING: 'WARNING', + INFO: 'INFO', + DEBUG: 'DEBUG', + NOTSET: 'NOTSET', +} +_nameToLevel = { + 'CRITICAL': CRITICAL, + 'FATAL': FATAL, + 'ERROR': ERROR, + 'WARN': WARNING, + 'WARNING': WARNING, + 'INFO': INFO, + 'DEBUG': DEBUG, + 'NOTSET': NOTSET, +} + +def getLevelName(level): + """ + Return the textual representation of logging level 'level'. + + If the level is one of the predefined levels (CRITICAL, ERROR, WARNING, + INFO, DEBUG) then you get the corresponding string. If you have + associated levels with names using addLevelName then the name you have + associated with 'level' is returned. + + If a numeric value corresponding to one of the defined levels is passed + in, the corresponding string representation is returned. + + Otherwise, the string "Level %s" % level is returned. + """ + # See Issues #22386, #27937 and #29220 for why it's this way + result = _levelToName.get(level) + if result is not None: + return result + result = _nameToLevel.get(level) + if result is not None: + return result + return "Level %s" % level + +def addLevelName(level, levelName): + """ + Associate 'levelName' with 'level'. + + This is used when converting levels to text during message formatting. + """ + _acquireLock() + try: #unlikely to cause an exception, but you never know... + _levelToName[level] = levelName + _nameToLevel[levelName] = level + finally: + _releaseLock() + +if hasattr(sys, '_getframe'): + currentframe = lambda: sys._getframe(3) +else: #pragma: no cover + def currentframe(): + """Return the frame object for the caller's stack frame.""" + try: + raise Exception + except Exception: + return sys.exc_info()[2].tb_frame.f_back + +# +# _srcfile is used when walking the stack to check when we've got the first +# caller stack frame, by skipping frames whose filename is that of this +# module's source. It therefore should contain the filename of this module's +# source file. +# +# Ordinarily we would use __file__ for this, but frozen modules don't always +# have __file__ set, for some reason (see Issue #21736). Thus, we get the +# filename from a handy code object from a function defined in this module. +# (There's no particular reason for picking addLevelName.) +# + +_srcfile = os.path.normcase(addLevelName.__code__.co_filename) + +# _srcfile is only used in conjunction with sys._getframe(). +# To provide compatibility with older versions of Python, set _srcfile +# to None if _getframe() is not available; this value will prevent +# findCaller() from being called. You can also do this if you want to avoid +# the overhead of fetching caller information, even when _getframe() is +# available. +#if not hasattr(sys, '_getframe'): +# _srcfile = None + + +def _checkLevel(level): + if isinstance(level, int): + rv = level + elif str(level) == level: + if level not in _nameToLevel: + raise ValueError("Unknown level: %r" % level) + rv = _nameToLevel[level] + else: + raise TypeError("Level not an integer or a valid string: %r" % level) + return rv + +#--------------------------------------------------------------------------- +# Thread-related stuff +#--------------------------------------------------------------------------- + +# +#_lock is used to serialize access to shared data structures in this module. +#This needs to be an RLock because fileConfig() creates and configures +#Handlers, and so might arbitrary user threads. Since Handler code updates the +#shared dictionary _handlers, it needs to acquire the lock. But if configuring, +#the lock would already have been acquired - so we need an RLock. +#The same argument applies to Loggers and Manager.loggerDict. +# +if threading: + _lock = threading.RLock() +else: #pragma: no cover + _lock = None + + +def _acquireLock(): + """ + Acquire the module-level lock for serializing access to shared data. + + This should be released with _releaseLock(). + """ + if _lock: + _lock.acquire() + +def _releaseLock(): + """ + Release the module-level lock acquired by calling _acquireLock(). + """ + if _lock: + _lock.release() + +#--------------------------------------------------------------------------- +# The logging record +#--------------------------------------------------------------------------- + +class LogRecord(object): + """ + A LogRecord instance represents an event being logged. + + LogRecord instances are created every time something is logged. They + contain all the information pertinent to the event being logged. The + main information passed in is in msg and args, which are combined + using str(msg) % args to create the message field of the record. The + record also includes information such as when the record was created, + the source line where the logging call was made, and any exception + information to be logged. + """ + def __init__(self, name, level, pathname, lineno, + msg, args, exc_info, func=None, sinfo=None, **kwargs): + """ + Initialize a logging record with interesting information. + """ + ct = time.time() + self.name = name + self.msg = msg + # + # The following statement allows passing of a dictionary as a sole + # argument, so that you can do something like + # logging.debug("a %(a)d b %(b)s", {'a':1, 'b':2}) + # Suggested by Stefan Behnel. + # Note that without the test for args[0], we get a problem because + # during formatting, we test to see if the arg is present using + # 'if self.args:'. If the event being logged is e.g. 'Value is %d' + # and if the passed arg fails 'if self.args:' then no formatting + # is done. For example, logger.warning('Value is %d', 0) would log + # 'Value is %d' instead of 'Value is 0'. + # For the use case of passing a dictionary, this should not be a + # problem. + # Issue #21172: a request was made to relax the isinstance check + # to hasattr(args[0], '__getitem__'). However, the docs on string + # formatting still seem to suggest a mapping object is required. + # Thus, while not removing the isinstance check, it does now look + # for collections.Mapping rather than, as before, dict. + if (args and len(args) == 1 and isinstance(args[0], collections.Mapping) + and args[0]): + args = args[0] + self.args = args + self.levelname = getLevelName(level) + self.levelno = level + self.pathname = pathname + try: + self.filename = os.path.basename(pathname) + self.module = os.path.splitext(self.filename)[0] + except (TypeError, ValueError, AttributeError): + self.filename = pathname + self.module = "Unknown module" + self.exc_info = exc_info + self.exc_text = None # used to cache the traceback text + self.stack_info = sinfo + self.lineno = lineno + self.funcName = func + self.created = ct + self.msecs = (ct - int(ct)) * 1000 + self.relativeCreated = (self.created - _startTime) * 1000 + if logThreads and threading: + self.thread = threading.get_ident() + self.threadName = threading.current_thread().name + else: # pragma: no cover + self.thread = None + self.threadName = None + if not logMultiprocessing: # pragma: no cover + self.processName = None + else: + self.processName = 'MainProcess' + mp = sys.modules.get('multiprocessing') + if mp is not None: + # Errors may occur if multiprocessing has not finished loading + # yet - e.g. if a custom import hook causes third-party code + # to run when multiprocessing calls import. See issue 8200 + # for an example + try: + self.processName = mp.current_process().name + except Exception: #pragma: no cover + pass + if logProcesses and hasattr(os, 'getpid'): + self.process = os.getpid() + else: + self.process = None + + def __str__(self): + return ''%(self.name, self.levelno, + self.pathname, self.lineno, self.msg) + + __repr__ = __str__ + + def getMessage(self): + """ + Return the message for this LogRecord. + + Return the message for this LogRecord after merging any user-supplied + arguments with the message. + """ + msg = str(self.msg) + if self.args: + msg = msg % self.args + return msg + +# +# Determine which class to use when instantiating log records. +# +_logRecordFactory = LogRecord + +def setLogRecordFactory(factory): + """ + Set the factory to be used when instantiating a log record. + + :param factory: A callable which will be called to instantiate + a log record. + """ + global _logRecordFactory + _logRecordFactory = factory + +def getLogRecordFactory(): + """ + Return the factory to be used when instantiating a log record. + """ + + return _logRecordFactory + +def makeLogRecord(dict): + """ + Make a LogRecord whose attributes are defined by the specified dictionary, + This function is useful for converting a logging event received over + a socket connection (which is sent as a dictionary) into a LogRecord + instance. + """ + rv = _logRecordFactory(None, None, "", 0, "", (), None, None) + rv.__dict__.update(dict) + return rv + +#--------------------------------------------------------------------------- +# Formatter classes and functions +#--------------------------------------------------------------------------- + +class PercentStyle(object): + + default_format = '%(message)s' + asctime_format = '%(asctime)s' + asctime_search = '%(asctime)' + + def __init__(self, fmt): + self._fmt = fmt or self.default_format + + def usesTime(self): + return self._fmt.find(self.asctime_search) >= 0 + + def format(self, record): + return self._fmt % record.__dict__ + +class StrFormatStyle(PercentStyle): + default_format = '{message}' + asctime_format = '{asctime}' + asctime_search = '{asctime' + + def format(self, record): + return self._fmt.format(**record.__dict__) + + +class StringTemplateStyle(PercentStyle): + default_format = '${message}' + asctime_format = '${asctime}' + asctime_search = '${asctime}' + + def __init__(self, fmt): + self._fmt = fmt or self.default_format + self._tpl = Template(self._fmt) + + def usesTime(self): + fmt = self._fmt + return fmt.find('$asctime') >= 0 or fmt.find(self.asctime_format) >= 0 + + def format(self, record): + return self._tpl.substitute(**record.__dict__) + +BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s" + +_STYLES = { + '%': (PercentStyle, BASIC_FORMAT), + '{': (StrFormatStyle, '{levelname}:{name}:{message}'), + '$': (StringTemplateStyle, '${levelname}:${name}:${message}'), +} + +class Formatter(object): + """ + Formatter instances are used to convert a LogRecord to text. + + Formatters need to know how a LogRecord is constructed. They are + responsible for converting a LogRecord to (usually) a string which can + be interpreted by either a human or an external system. The base Formatter + allows a formatting string to be specified. If none is supplied, the + the style-dependent default value, "%(message)s", "{message}", or + "${message}", is used. + + The Formatter can be initialized with a format string which makes use of + knowledge of the LogRecord attributes - e.g. the default value mentioned + above makes use of the fact that the user's message and arguments are pre- + formatted into a LogRecord's message attribute. Currently, the useful + attributes in a LogRecord are described by: + + %(name)s Name of the logger (logging channel) + %(levelno)s Numeric logging level for the message (DEBUG, INFO, + WARNING, ERROR, CRITICAL) + %(levelname)s Text logging level for the message ("DEBUG", "INFO", + "WARNING", "ERROR", "CRITICAL") + %(pathname)s Full pathname of the source file where the logging + call was issued (if available) + %(filename)s Filename portion of pathname + %(module)s Module (name portion of filename) + %(lineno)d Source line number where the logging call was issued + (if available) + %(funcName)s Function name + %(created)f Time when the LogRecord was created (time.time() + return value) + %(asctime)s Textual time when the LogRecord was created + %(msecs)d Millisecond portion of the creation time + %(relativeCreated)d Time in milliseconds when the LogRecord was created, + relative to the time the logging module was loaded + (typically at application startup time) + %(thread)d Thread ID (if available) + %(threadName)s Thread name (if available) + %(process)d Process ID (if available) + %(message)s The result of record.getMessage(), computed just as + the record is emitted + """ + + converter = time.localtime + + def __init__(self, fmt=None, datefmt=None, style='%'): + """ + Initialize the formatter with specified format strings. + + Initialize the formatter either with the specified format string, or a + default as described above. Allow for specialized date formatting with + the optional datefmt argument. If datefmt is omitted, you get an + ISO8601-like (or RFC 3339-like) format. + + Use a style parameter of '%', '{' or '$' to specify that you want to + use one of %-formatting, :meth:`str.format` (``{}``) formatting or + :class:`string.Template` formatting in your format string. + + .. versionchanged:: 3.2 + Added the ``style`` parameter. + """ + if style not in _STYLES: + raise ValueError('Style must be one of: %s' % ','.join( + _STYLES.keys())) + self._style = _STYLES[style][0](fmt) + self._fmt = self._style._fmt + self.datefmt = datefmt + + default_time_format = '%Y-%m-%d %H:%M:%S' + default_msec_format = '%s,%03d' + + def formatTime(self, record, datefmt=None): + """ + Return the creation time of the specified LogRecord as formatted text. + + This method should be called from format() by a formatter which + wants to make use of a formatted time. This method can be overridden + in formatters to provide for any specific requirement, but the + basic behaviour is as follows: if datefmt (a string) is specified, + it is used with time.strftime() to format the creation time of the + record. Otherwise, an ISO8601-like (or RFC 3339-like) format is used. + The resulting string is returned. This function uses a user-configurable + function to convert the creation time to a tuple. By default, + time.localtime() is used; to change this for a particular formatter + instance, set the 'converter' attribute to a function with the same + signature as time.localtime() or time.gmtime(). To change it for all + formatters, for example if you want all logging times to be shown in GMT, + set the 'converter' attribute in the Formatter class. + """ + ct = self.converter(record.created) + if datefmt: + s = time.strftime(datefmt, ct) + else: + t = time.strftime(self.default_time_format, ct) + s = self.default_msec_format % (t, record.msecs) + return s + + def formatException(self, ei): + """ + Format and return the specified exception information as a string. + + This default implementation just uses + traceback.print_exception() + """ + sio = io.StringIO() + tb = ei[2] + # See issues #9427, #1553375. Commented out for now. + #if getattr(self, 'fullstack', False): + # traceback.print_stack(tb.tb_frame.f_back, file=sio) + traceback.print_exception(ei[0], ei[1], tb, None, sio) + s = sio.getvalue() + sio.close() + if s[-1:] == "\n": + s = s[:-1] + return s + + def usesTime(self): + """ + Check if the format uses the creation time of the record. + """ + return self._style.usesTime() + + def formatMessage(self, record): + return self._style.format(record) + + def formatStack(self, stack_info): + """ + This method is provided as an extension point for specialized + formatting of stack information. + + The input data is a string as returned from a call to + :func:`traceback.print_stack`, but with the last trailing newline + removed. + + The base implementation just returns the value passed in. + """ + return stack_info + + def format(self, record): + """ + Format the specified record as text. + + The record's attribute dictionary is used as the operand to a + string formatting operation which yields the returned string. + Before formatting the dictionary, a couple of preparatory steps + are carried out. The message attribute of the record is computed + using LogRecord.getMessage(). If the formatting string uses the + time (as determined by a call to usesTime(), formatTime() is + called to format the event time. If there is exception information, + it is formatted using formatException() and appended to the message. + """ + record.message = record.getMessage() + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + s = self.formatMessage(record) + if record.exc_info: + # Cache the traceback text to avoid converting it multiple times + # (it's constant anyway) + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + if record.exc_text: + if s[-1:] != "\n": + s = s + "\n" + s = s + record.exc_text + if record.stack_info: + if s[-1:] != "\n": + s = s + "\n" + s = s + self.formatStack(record.stack_info) + return s + +# +# The default formatter to use when no other is specified +# +_defaultFormatter = Formatter() + +class BufferingFormatter(object): + """ + A formatter suitable for formatting a number of records. + """ + def __init__(self, linefmt=None): + """ + Optionally specify a formatter which will be used to format each + individual record. + """ + if linefmt: + self.linefmt = linefmt + else: + self.linefmt = _defaultFormatter + + def formatHeader(self, records): + """ + Return the header string for the specified records. + """ + return "" + + def formatFooter(self, records): + """ + Return the footer string for the specified records. + """ + return "" + + def format(self, records): + """ + Format the specified records and return the result as a string. + """ + rv = "" + if len(records) > 0: + rv = rv + self.formatHeader(records) + for record in records: + rv = rv + self.linefmt.format(record) + rv = rv + self.formatFooter(records) + return rv + +#--------------------------------------------------------------------------- +# Filter classes and functions +#--------------------------------------------------------------------------- + +class Filter(object): + """ + Filter instances are used to perform arbitrary filtering of LogRecords. + + Loggers and Handlers can optionally use Filter instances to filter + records as desired. The base filter class only allows events which are + below a certain point in the logger hierarchy. For example, a filter + initialized with "A.B" will allow events logged by loggers "A.B", + "A.B.C", "A.B.C.D", "A.B.D" etc. but not "A.BB", "B.A.B" etc. If + initialized with the empty string, all events are passed. + """ + def __init__(self, name=''): + """ + Initialize a filter. + + Initialize with the name of the logger which, together with its + children, will have its events allowed through the filter. If no + name is specified, allow every event. + """ + self.name = name + self.nlen = len(name) + + def filter(self, record): + """ + Determine if the specified record is to be logged. + + Is the specified record to be logged? Returns 0 for no, nonzero for + yes. If deemed appropriate, the record may be modified in-place. + """ + if self.nlen == 0: + return True + elif self.name == record.name: + return True + elif record.name.find(self.name, 0, self.nlen) != 0: + return False + return (record.name[self.nlen] == ".") + +class Filterer(object): + """ + A base class for loggers and handlers which allows them to share + common code. + """ + def __init__(self): + """ + Initialize the list of filters to be an empty list. + """ + self.filters = [] + + def addFilter(self, filter): + """ + Add the specified filter to this handler. + """ + if not (filter in self.filters): + self.filters.append(filter) + + def removeFilter(self, filter): + """ + Remove the specified filter from this handler. + """ + if filter in self.filters: + self.filters.remove(filter) + + def filter(self, record): + """ + Determine if a record is loggable by consulting all the filters. + + The default is to allow the record to be logged; any filter can veto + this and the record is then dropped. Returns a zero value if a record + is to be dropped, else non-zero. + + .. versionchanged:: 3.2 + + Allow filters to be just callables. + """ + rv = True + for f in self.filters: + if hasattr(f, 'filter'): + result = f.filter(record) + else: + result = f(record) # assume callable - will raise if not + if not result: + rv = False + break + return rv + +#--------------------------------------------------------------------------- +# Handler classes and functions +#--------------------------------------------------------------------------- + +_handlers = weakref.WeakValueDictionary() #map of handler names to handlers +_handlerList = [] # added to allow handlers to be removed in reverse of order initialized + +def _removeHandlerRef(wr): + """ + Remove a handler reference from the internal cleanup list. + """ + # This function can be called during module teardown, when globals are + # set to None. It can also be called from another thread. So we need to + # pre-emptively grab the necessary globals and check if they're None, + # to prevent race conditions and failures during interpreter shutdown. + acquire, release, handlers = _acquireLock, _releaseLock, _handlerList + if acquire and release and handlers: + acquire() + try: + if wr in handlers: + handlers.remove(wr) + finally: + release() + +def _addHandlerRef(handler): + """ + Add a handler to the internal cleanup list using a weak reference. + """ + _acquireLock() + try: + _handlerList.append(weakref.ref(handler, _removeHandlerRef)) + finally: + _releaseLock() + +class Handler(Filterer): + """ + Handler instances dispatch logging events to specific destinations. + + The base handler class. Acts as a placeholder which defines the Handler + interface. Handlers can optionally use Formatter instances to format + records as desired. By default, no formatter is specified; in this case, + the 'raw' message as determined by record.message is logged. + """ + def __init__(self, level=NOTSET): + """ + Initializes the instance - basically setting the formatter to None + and the filter list to empty. + """ + Filterer.__init__(self) + self._name = None + self.level = _checkLevel(level) + self.formatter = None + # Add the handler to the global _handlerList (for cleanup on shutdown) + _addHandlerRef(self) + self.createLock() + + def get_name(self): + return self._name + + def set_name(self, name): + _acquireLock() + try: + if self._name in _handlers: + del _handlers[self._name] + self._name = name + if name: + _handlers[name] = self + finally: + _releaseLock() + + name = property(get_name, set_name) + + def createLock(self): + """ + Acquire a thread lock for serializing access to the underlying I/O. + """ + if threading: + self.lock = threading.RLock() + else: #pragma: no cover + self.lock = None + + def acquire(self): + """ + Acquire the I/O thread lock. + """ + if self.lock: + self.lock.acquire() + + def release(self): + """ + Release the I/O thread lock. + """ + if self.lock: + self.lock.release() + + def setLevel(self, level): + """ + Set the logging level of this handler. level must be an int or a str. + """ + self.level = _checkLevel(level) + + def format(self, record): + """ + Format the specified record. + + If a formatter is set, use it. Otherwise, use the default formatter + for the module. + """ + if self.formatter: + fmt = self.formatter + else: + fmt = _defaultFormatter + return fmt.format(record) + + def emit(self, record): + """ + Do whatever it takes to actually log the specified logging record. + + This version is intended to be implemented by subclasses and so + raises a NotImplementedError. + """ + raise NotImplementedError('emit must be implemented ' + 'by Handler subclasses') + + def handle(self, record): + """ + Conditionally emit the specified logging record. + + Emission depends on filters which may have been added to the handler. + Wrap the actual emission of the record with acquisition/release of + the I/O thread lock. Returns whether the filter passed the record for + emission. + """ + rv = self.filter(record) + if rv: + self.acquire() + try: + self.emit(record) + finally: + self.release() + return rv + + def setFormatter(self, fmt): + """ + Set the formatter for this handler. + """ + self.formatter = fmt + + def flush(self): + """ + Ensure all logging output has been flushed. + + This version does nothing and is intended to be implemented by + subclasses. + """ + pass + + def close(self): + """ + Tidy up any resources used by the handler. + + This version removes the handler from an internal map of handlers, + _handlers, which is used for handler lookup by name. Subclasses + should ensure that this gets called from overridden close() + methods. + """ + #get the module data lock, as we're updating a shared structure. + _acquireLock() + try: #unlikely to raise an exception, but you never know... + if self._name and self._name in _handlers: + del _handlers[self._name] + finally: + _releaseLock() + + def handleError(self, record): + """ + Handle errors which occur during an emit() call. + + This method should be called from handlers when an exception is + encountered during an emit() call. If raiseExceptions is false, + exceptions get silently ignored. This is what is mostly wanted + for a logging system - most users will not care about errors in + the logging system, they are more interested in application errors. + You could, however, replace this with a custom handler if you wish. + The record which was being processed is passed in to this method. + """ + if raiseExceptions and sys.stderr: # see issue 13807 + t, v, tb = sys.exc_info() + try: + sys.stderr.write('--- Logging error ---\n') + traceback.print_exception(t, v, tb, None, sys.stderr) + sys.stderr.write('Call stack:\n') + # Walk the stack frame up until we're out of logging, + # so as to print the calling context. + frame = tb.tb_frame + while (frame and os.path.dirname(frame.f_code.co_filename) == + __path__[0]): + frame = frame.f_back + if frame: + traceback.print_stack(frame, file=sys.stderr) + else: + # couldn't find the right stack frame, for some reason + sys.stderr.write('Logged from file %s, line %s\n' % ( + record.filename, record.lineno)) + # Issue 18671: output logging message and arguments + try: + sys.stderr.write('Message: %r\n' + 'Arguments: %s\n' % (record.msg, + record.args)) + except Exception: + sys.stderr.write('Unable to print the message and arguments' + ' - possible formatting error.\nUse the' + ' traceback above to help find the error.\n' + ) + except OSError: #pragma: no cover + pass # see issue 5971 + finally: + del t, v, tb + + def __repr__(self): + level = getLevelName(self.level) + return '<%s (%s)>' % (self.__class__.__name__, level) + +class StreamHandler(Handler): + """ + A handler class which writes logging records, appropriately formatted, + to a stream. Note that this class does not close the stream, as + sys.stdout or sys.stderr may be used. + """ + + terminator = '\n' + + def __init__(self, stream=None): + """ + Initialize the handler. + + If stream is not specified, sys.stderr is used. + """ + Handler.__init__(self) + if stream is None: + stream = sys.stderr + self.stream = stream + + def flush(self): + """ + Flushes the stream. + """ + self.acquire() + try: + if self.stream and hasattr(self.stream, "flush"): + self.stream.flush() + finally: + self.release() + + def emit(self, record): + """ + Emit a record. + + If a formatter is specified, it is used to format the record. + The record is then written to the stream with a trailing newline. If + exception information is present, it is formatted using + traceback.print_exception and appended to the stream. If the stream + has an 'encoding' attribute, it is used to determine how to do the + output to the stream. + """ + try: + msg = self.format(record) + stream = self.stream + stream.write(msg) + stream.write(self.terminator) + self.flush() + except Exception: + self.handleError(record) + + def __repr__(self): + level = getLevelName(self.level) + name = getattr(self.stream, 'name', '') + if name: + name += ' ' + return '<%s %s(%s)>' % (self.__class__.__name__, name, level) + + +class FileHandler(StreamHandler): + """ + A handler class which writes formatted logging records to disk files. + """ + def __init__(self, filename, mode='a', encoding=None, delay=False): + """ + Open the specified file and use it as the stream for logging. + """ + # Issue #27493: add support for Path objects to be passed in + filename = os.fspath(filename) + #keep the absolute path, otherwise derived classes which use this + #may come a cropper when the current directory changes + self.baseFilename = os.path.abspath(filename) + self.mode = mode + self.encoding = encoding + self.delay = delay + if delay: + #We don't open the stream, but we still need to call the + #Handler constructor to set level, formatter, lock etc. + Handler.__init__(self) + self.stream = None + else: + StreamHandler.__init__(self, self._open()) + + def close(self): + """ + Closes the stream. + """ + self.acquire() + try: + try: + if self.stream: + try: + self.flush() + finally: + stream = self.stream + self.stream = None + if hasattr(stream, "close"): + stream.close() + finally: + # Issue #19523: call unconditionally to + # prevent a handler leak when delay is set + StreamHandler.close(self) + finally: + self.release() + + def _open(self): + """ + Open the current base file with the (original) mode and encoding. + Return the resulting stream. + """ + return open(self.baseFilename, self.mode, encoding=self.encoding) + + def emit(self, record): + """ + Emit a record. + + If the stream was not opened because 'delay' was specified in the + constructor, open it before calling the superclass's emit. + """ + if self.stream is None: + self.stream = self._open() + StreamHandler.emit(self, record) + + def __repr__(self): + level = getLevelName(self.level) + return '<%s %s (%s)>' % (self.__class__.__name__, self.baseFilename, level) + + +class _StderrHandler(StreamHandler): + """ + This class is like a StreamHandler using sys.stderr, but always uses + whatever sys.stderr is currently set to rather than the value of + sys.stderr at handler construction time. + """ + def __init__(self, level=NOTSET): + """ + Initialize the handler. + """ + Handler.__init__(self, level) + + @property + def stream(self): + return sys.stderr + + +_defaultLastResort = _StderrHandler(WARNING) +lastResort = _defaultLastResort + +#--------------------------------------------------------------------------- +# Manager classes and functions +#--------------------------------------------------------------------------- + +class PlaceHolder(object): + """ + PlaceHolder instances are used in the Manager logger hierarchy to take + the place of nodes for which no loggers have been defined. This class is + intended for internal use only and not as part of the public API. + """ + def __init__(self, alogger): + """ + Initialize with the specified logger being a child of this placeholder. + """ + self.loggerMap = { alogger : None } + + def append(self, alogger): + """ + Add the specified logger as a child of this placeholder. + """ + if alogger not in self.loggerMap: + self.loggerMap[alogger] = None + +# +# Determine which class to use when instantiating loggers. +# + +def setLoggerClass(klass): + """ + Set the class to be used when instantiating a logger. The class should + define __init__() such that only a name argument is required, and the + __init__() should call Logger.__init__() + """ + if klass != Logger: + if not issubclass(klass, Logger): + raise TypeError("logger not derived from logging.Logger: " + + klass.__name__) + global _loggerClass + _loggerClass = klass + +def getLoggerClass(): + """ + Return the class to be used when instantiating a logger. + """ + return _loggerClass + +class Manager(object): + """ + There is [under normal circumstances] just one Manager instance, which + holds the hierarchy of loggers. + """ + def __init__(self, rootnode): + """ + Initialize the manager with the root node of the logger hierarchy. + """ + self.root = rootnode + self.disable = 0 + self.emittedNoHandlerWarning = False + self.loggerDict = {} + self.loggerClass = None + self.logRecordFactory = None + + def getLogger(self, name): + """ + Get a logger with the specified name (channel name), creating it + if it doesn't yet exist. This name is a dot-separated hierarchical + name, such as "a", "a.b", "a.b.c" or similar. + + If a PlaceHolder existed for the specified name [i.e. the logger + didn't exist but a child of it did], replace it with the created + logger and fix up the parent/child references which pointed to the + placeholder to now point to the logger. + """ + rv = None + if not isinstance(name, str): + raise TypeError('A logger name must be a string') + _acquireLock() + try: + if name in self.loggerDict: + rv = self.loggerDict[name] + if isinstance(rv, PlaceHolder): + ph = rv + rv = (self.loggerClass or _loggerClass)(name) + rv.manager = self + self.loggerDict[name] = rv + self._fixupChildren(ph, rv) + self._fixupParents(rv) + else: + rv = (self.loggerClass or _loggerClass)(name) + rv.manager = self + self.loggerDict[name] = rv + self._fixupParents(rv) + finally: + _releaseLock() + return rv + + def setLoggerClass(self, klass): + """ + Set the class to be used when instantiating a logger with this Manager. + """ + if klass != Logger: + if not issubclass(klass, Logger): + raise TypeError("logger not derived from logging.Logger: " + + klass.__name__) + self.loggerClass = klass + + def setLogRecordFactory(self, factory): + """ + Set the factory to be used when instantiating a log record with this + Manager. + """ + self.logRecordFactory = factory + + def _fixupParents(self, alogger): + """ + Ensure that there are either loggers or placeholders all the way + from the specified logger to the root of the logger hierarchy. + """ + name = alogger.name + i = name.rfind(".") + rv = None + while (i > 0) and not rv: + substr = name[:i] + if substr not in self.loggerDict: + self.loggerDict[substr] = PlaceHolder(alogger) + else: + obj = self.loggerDict[substr] + if isinstance(obj, Logger): + rv = obj + else: + assert isinstance(obj, PlaceHolder) + obj.append(alogger) + i = name.rfind(".", 0, i - 1) + if not rv: + rv = self.root + alogger.parent = rv + + def _fixupChildren(self, ph, alogger): + """ + Ensure that children of the placeholder ph are connected to the + specified logger. + """ + name = alogger.name + namelen = len(name) + for c in ph.loggerMap.keys(): + #The if means ... if not c.parent.name.startswith(nm) + if c.parent.name[:namelen] != name: + alogger.parent = c.parent + c.parent = alogger + +#--------------------------------------------------------------------------- +# Logger classes and functions +#--------------------------------------------------------------------------- + +class Logger(Filterer): + """ + Instances of the Logger class represent a single logging channel. A + "logging channel" indicates an area of an application. Exactly how an + "area" is defined is up to the application developer. Since an + application can have any number of areas, logging channels are identified + by a unique string. Application areas can be nested (e.g. an area + of "input processing" might include sub-areas "read CSV files", "read + XLS files" and "read Gnumeric files"). To cater for this natural nesting, + channel names are organized into a namespace hierarchy where levels are + separated by periods, much like the Java or Python package namespace. So + in the instance given above, channel names might be "input" for the upper + level, and "input.csv", "input.xls" and "input.gnu" for the sub-levels. + There is no arbitrary limit to the depth of nesting. + """ + def __init__(self, name, level=NOTSET): + """ + Initialize the logger with a name and an optional level. + """ + Filterer.__init__(self) + self.name = name + self.level = _checkLevel(level) + self.parent = None + self.propagate = True + self.handlers = [] + self.disabled = False + + def setLevel(self, level): + """ + Set the logging level of this logger. level must be an int or a str. + """ + self.level = _checkLevel(level) + + def debug(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'DEBUG'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.debug("Houston, we have a %s", "thorny problem", exc_info=1) + """ + if self.isEnabledFor(DEBUG): + self._log(DEBUG, msg, args, **kwargs) + + def info(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'INFO'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.info("Houston, we have a %s", "interesting problem", exc_info=1) + """ + if self.isEnabledFor(INFO): + self._log(INFO, msg, args, **kwargs) + + def warning(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'WARNING'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.warning("Houston, we have a %s", "bit of a problem", exc_info=1) + """ + if self.isEnabledFor(WARNING): + self._log(WARNING, msg, args, **kwargs) + + def warn(self, msg, *args, **kwargs): + warnings.warn("The 'warn' method is deprecated, " + "use 'warning' instead", DeprecationWarning, 2) + self.warning(msg, *args, **kwargs) + + def error(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'ERROR'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.error("Houston, we have a %s", "major problem", exc_info=1) + """ + if self.isEnabledFor(ERROR): + self._log(ERROR, msg, args, **kwargs) + + def exception(self, msg, *args, exc_info=True, **kwargs): + """ + Convenience method for logging an ERROR with exception information. + """ + self.error(msg, *args, exc_info=exc_info, **kwargs) + + def critical(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'CRITICAL'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.critical("Houston, we have a %s", "major disaster", exc_info=1) + """ + if self.isEnabledFor(CRITICAL): + self._log(CRITICAL, msg, args, **kwargs) + + fatal = critical + + def log(self, level, msg, *args, **kwargs): + """ + Log 'msg % args' with the integer severity 'level'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.log(level, "We have a %s", "mysterious problem", exc_info=1) + """ + if not isinstance(level, int): + if raiseExceptions: + raise TypeError("level must be an integer") + else: + return + if self.isEnabledFor(level): + self._log(level, msg, args, **kwargs) + + def findCaller(self, stack_info=False): + """ + Find the stack frame of the caller so that we can note the source + file name, line number and function name. + """ + f = currentframe() + #On some versions of IronPython, currentframe() returns None if + #IronPython isn't run with -X:Frames. + if f is not None: + f = f.f_back + rv = "(unknown file)", 0, "(unknown function)", None + while hasattr(f, "f_code"): + co = f.f_code + filename = os.path.normcase(co.co_filename) + if filename == _srcfile: + f = f.f_back + continue + sinfo = None + if stack_info: + sio = io.StringIO() + sio.write('Stack (most recent call last):\n') + traceback.print_stack(f, file=sio) + sinfo = sio.getvalue() + if sinfo[-1] == '\n': + sinfo = sinfo[:-1] + sio.close() + rv = (co.co_filename, f.f_lineno, co.co_name, sinfo) + break + return rv + + def makeRecord(self, name, level, fn, lno, msg, args, exc_info, + func=None, extra=None, sinfo=None): + """ + A factory method which can be overridden in subclasses to create + specialized LogRecords. + """ + rv = _logRecordFactory(name, level, fn, lno, msg, args, exc_info, func, + sinfo) + if extra is not None: + for key in extra: + if (key in ["message", "asctime"]) or (key in rv.__dict__): + raise KeyError("Attempt to overwrite %r in LogRecord" % key) + rv.__dict__[key] = extra[key] + return rv + + def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False): + """ + Low-level logging routine which creates a LogRecord and then calls + all the handlers of this logger to handle the record. + """ + sinfo = None + if _srcfile: + #IronPython doesn't track Python frames, so findCaller raises an + #exception on some versions of IronPython. We trap it here so that + #IronPython can use logging. + try: + fn, lno, func, sinfo = self.findCaller(stack_info) + except ValueError: # pragma: no cover + fn, lno, func = "(unknown file)", 0, "(unknown function)" + else: # pragma: no cover + fn, lno, func = "(unknown file)", 0, "(unknown function)" + if exc_info: + if isinstance(exc_info, BaseException): + exc_info = (type(exc_info), exc_info, exc_info.__traceback__) + elif not isinstance(exc_info, tuple): + exc_info = sys.exc_info() + record = self.makeRecord(self.name, level, fn, lno, msg, args, + exc_info, func, extra, sinfo) + self.handle(record) + + def handle(self, record): + """ + Call the handlers for the specified record. + + This method is used for unpickled records received from a socket, as + well as those created locally. Logger-level filtering is applied. + """ + if (not self.disabled) and self.filter(record): + self.callHandlers(record) + + def addHandler(self, hdlr): + """ + Add the specified handler to this logger. + """ + _acquireLock() + try: + if not (hdlr in self.handlers): + self.handlers.append(hdlr) + finally: + _releaseLock() + + def removeHandler(self, hdlr): + """ + Remove the specified handler from this logger. + """ + _acquireLock() + try: + if hdlr in self.handlers: + self.handlers.remove(hdlr) + finally: + _releaseLock() + + def hasHandlers(self): + """ + See if this logger has any handlers configured. + + Loop through all handlers for this logger and its parents in the + logger hierarchy. Return True if a handler was found, else False. + Stop searching up the hierarchy whenever a logger with the "propagate" + attribute set to zero is found - that will be the last logger which + is checked for the existence of handlers. + """ + c = self + rv = False + while c: + if c.handlers: + rv = True + break + if not c.propagate: + break + else: + c = c.parent + return rv + + def callHandlers(self, record): + """ + Pass a record to all relevant handlers. + + Loop through all handlers for this logger and its parents in the + logger hierarchy. If no handler was found, output a one-off error + message to sys.stderr. Stop searching up the hierarchy whenever a + logger with the "propagate" attribute set to zero is found - that + will be the last logger whose handlers are called. + """ + c = self + found = 0 + while c: + for hdlr in c.handlers: + found = found + 1 + if record.levelno >= hdlr.level: + hdlr.handle(record) + if not c.propagate: + c = None #break out + else: + c = c.parent + if (found == 0): + if lastResort: + if record.levelno >= lastResort.level: + lastResort.handle(record) + elif raiseExceptions and not self.manager.emittedNoHandlerWarning: + sys.stderr.write("No handlers could be found for logger" + " \"%s\"\n" % self.name) + self.manager.emittedNoHandlerWarning = True + + def getEffectiveLevel(self): + """ + Get the effective level for this logger. + + Loop through this logger and its parents in the logger hierarchy, + looking for a non-zero logging level. Return the first one found. + """ + logger = self + while logger: + if logger.level: + return logger.level + logger = logger.parent + return NOTSET + + def isEnabledFor(self, level): + """ + Is this logger enabled for level 'level'? + """ + if self.manager.disable >= level: + return False + return level >= self.getEffectiveLevel() + + def getChild(self, suffix): + """ + Get a logger which is a descendant to this one. + + This is a convenience method, such that + + logging.getLogger('abc').getChild('def.ghi') + + is the same as + + logging.getLogger('abc.def.ghi') + + It's useful, for example, when the parent logger is named using + __name__ rather than a literal string. + """ + if self.root is not self: + suffix = '.'.join((self.name, suffix)) + return self.manager.getLogger(suffix) + + def __repr__(self): + level = getLevelName(self.getEffectiveLevel()) + return '<%s %s (%s)>' % (self.__class__.__name__, self.name, level) + + +class RootLogger(Logger): + """ + A root logger is not that different to any other logger, except that + it must have a logging level and there is only one instance of it in + the hierarchy. + """ + def __init__(self, level): + """ + Initialize the logger with the name "root". + """ + Logger.__init__(self, "root", level) + +_loggerClass = Logger + +class LoggerAdapter(object): + """ + An adapter for loggers which makes it easier to specify contextual + information in logging output. + """ + + def __init__(self, logger, extra): + """ + Initialize the adapter with a logger and a dict-like object which + provides contextual information. This constructor signature allows + easy stacking of LoggerAdapters, if so desired. + + You can effectively pass keyword arguments as shown in the + following example: + + adapter = LoggerAdapter(someLogger, dict(p1=v1, p2="v2")) + """ + self.logger = logger + self.extra = extra + + def process(self, msg, kwargs): + """ + Process the logging message and keyword arguments passed in to + a logging call to insert contextual information. You can either + manipulate the message itself, the keyword args or both. Return + the message and kwargs modified (or not) to suit your needs. + + Normally, you'll only need to override this one method in a + LoggerAdapter subclass for your specific needs. + """ + kwargs["extra"] = self.extra + return msg, kwargs + + # + # Boilerplate convenience methods + # + def debug(self, msg, *args, **kwargs): + """ + Delegate a debug call to the underlying logger. + """ + self.log(DEBUG, msg, *args, **kwargs) + + def info(self, msg, *args, **kwargs): + """ + Delegate an info call to the underlying logger. + """ + self.log(INFO, msg, *args, **kwargs) + + def warning(self, msg, *args, **kwargs): + """ + Delegate a warning call to the underlying logger. + """ + self.log(WARNING, msg, *args, **kwargs) + + def warn(self, msg, *args, **kwargs): + warnings.warn("The 'warn' method is deprecated, " + "use 'warning' instead", DeprecationWarning, 2) + self.warning(msg, *args, **kwargs) + + def error(self, msg, *args, **kwargs): + """ + Delegate an error call to the underlying logger. + """ + self.log(ERROR, msg, *args, **kwargs) + + def exception(self, msg, *args, exc_info=True, **kwargs): + """ + Delegate an exception call to the underlying logger. + """ + self.log(ERROR, msg, *args, exc_info=exc_info, **kwargs) + + def critical(self, msg, *args, **kwargs): + """ + Delegate a critical call to the underlying logger. + """ + self.log(CRITICAL, msg, *args, **kwargs) + + def log(self, level, msg, *args, **kwargs): + """ + Delegate a log call to the underlying logger, after adding + contextual information from this adapter instance. + """ + if self.isEnabledFor(level): + msg, kwargs = self.process(msg, kwargs) + self.logger.log(level, msg, *args, **kwargs) + + def isEnabledFor(self, level): + """ + Is this logger enabled for level 'level'? + """ + if self.logger.manager.disable >= level: + return False + return level >= self.getEffectiveLevel() + + def setLevel(self, level): + """ + Set the specified level on the underlying logger. + """ + self.logger.setLevel(level) + + def getEffectiveLevel(self): + """ + Get the effective level for the underlying logger. + """ + return self.logger.getEffectiveLevel() + + def hasHandlers(self): + """ + See if the underlying logger has any handlers. + """ + return self.logger.hasHandlers() + + def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False): + """ + Low-level log implementation, proxied to allow nested logger adapters. + """ + return self.logger._log( + level, + msg, + args, + exc_info=exc_info, + extra=extra, + stack_info=stack_info, + ) + + @property + def manager(self): + return self.logger.manager + + @manager.setter + def manager(self, value): + self.logger.manager = value + + @property + def name(self): + return self.logger.name + + def __repr__(self): + logger = self.logger + level = getLevelName(logger.getEffectiveLevel()) + return '<%s %s (%s)>' % (self.__class__.__name__, logger.name, level) + +root = RootLogger(WARNING) +Logger.root = root +Logger.manager = Manager(Logger.root) + +#--------------------------------------------------------------------------- +# Configuration classes and functions +#--------------------------------------------------------------------------- + +def basicConfig(**kwargs): + """ + Do basic configuration for the logging system. + + This function does nothing if the root logger already has handlers + configured. It is a convenience method intended for use by simple scripts + to do one-shot configuration of the logging package. + + The default behaviour is to create a StreamHandler which writes to + sys.stderr, set a formatter using the BASIC_FORMAT format string, and + add the handler to the root logger. + + A number of optional keyword arguments may be specified, which can alter + the default behaviour. + + filename Specifies that a FileHandler be created, using the specified + filename, rather than a StreamHandler. + filemode Specifies the mode to open the file, if filename is specified + (if filemode is unspecified, it defaults to 'a'). + format Use the specified format string for the handler. + datefmt Use the specified date/time format. + style If a format string is specified, use this to specify the + type of format string (possible values '%', '{', '$', for + %-formatting, :meth:`str.format` and :class:`string.Template` + - defaults to '%'). + level Set the root logger level to the specified level. + stream Use the specified stream to initialize the StreamHandler. Note + that this argument is incompatible with 'filename' - if both + are present, 'stream' is ignored. + handlers If specified, this should be an iterable of already created + handlers, which will be added to the root handler. Any handler + in the list which does not have a formatter assigned will be + assigned the formatter created in this function. + + Note that you could specify a stream created using open(filename, mode) + rather than passing the filename and mode in. However, it should be + remembered that StreamHandler does not close its stream (since it may be + using sys.stdout or sys.stderr), whereas FileHandler closes its stream + when the handler is closed. + + .. versionchanged:: 3.2 + Added the ``style`` parameter. + + .. versionchanged:: 3.3 + Added the ``handlers`` parameter. A ``ValueError`` is now thrown for + incompatible arguments (e.g. ``handlers`` specified together with + ``filename``/``filemode``, or ``filename``/``filemode`` specified + together with ``stream``, or ``handlers`` specified together with + ``stream``. + """ + # Add thread safety in case someone mistakenly calls + # basicConfig() from multiple threads + _acquireLock() + try: + if len(root.handlers) == 0: + handlers = kwargs.pop("handlers", None) + if handlers is None: + if "stream" in kwargs and "filename" in kwargs: + raise ValueError("'stream' and 'filename' should not be " + "specified together") + else: + if "stream" in kwargs or "filename" in kwargs: + raise ValueError("'stream' or 'filename' should not be " + "specified together with 'handlers'") + if handlers is None: + filename = kwargs.pop("filename", None) + mode = kwargs.pop("filemode", 'a') + if filename: + h = FileHandler(filename, mode) + else: + stream = kwargs.pop("stream", None) + h = StreamHandler(stream) + handlers = [h] + dfs = kwargs.pop("datefmt", None) + style = kwargs.pop("style", '%') + if style not in _STYLES: + raise ValueError('Style must be one of: %s' % ','.join( + _STYLES.keys())) + fs = kwargs.pop("format", _STYLES[style][1]) + fmt = Formatter(fs, dfs, style) + for h in handlers: + if h.formatter is None: + h.setFormatter(fmt) + root.addHandler(h) + level = kwargs.pop("level", None) + if level is not None: + root.setLevel(level) + if kwargs: + keys = ', '.join(kwargs.keys()) + raise ValueError('Unrecognised argument(s): %s' % keys) + finally: + _releaseLock() + +#--------------------------------------------------------------------------- +# Utility functions at module level. +# Basically delegate everything to the root logger. +#--------------------------------------------------------------------------- + +def getLogger(name=None): + """ + Return a logger with the specified name, creating it if necessary. + + If no name is specified, return the root logger. + """ + if name: + return Logger.manager.getLogger(name) + else: + return root + +def critical(msg, *args, **kwargs): + """ + Log a message with severity 'CRITICAL' on the root logger. If the logger + has no handlers, call basicConfig() to add a console handler with a + pre-defined format. + """ + if len(root.handlers) == 0: + basicConfig() + root.critical(msg, *args, **kwargs) + +fatal = critical + +def error(msg, *args, **kwargs): + """ + Log a message with severity 'ERROR' on the root logger. If the logger has + no handlers, call basicConfig() to add a console handler with a pre-defined + format. + """ + if len(root.handlers) == 0: + basicConfig() + root.error(msg, *args, **kwargs) + +def exception(msg, *args, exc_info=True, **kwargs): + """ + Log a message with severity 'ERROR' on the root logger, with exception + information. If the logger has no handlers, basicConfig() is called to add + a console handler with a pre-defined format. + """ + error(msg, *args, exc_info=exc_info, **kwargs) + +def warning(msg, *args, **kwargs): + """ + Log a message with severity 'WARNING' on the root logger. If the logger has + no handlers, call basicConfig() to add a console handler with a pre-defined + format. + """ + if len(root.handlers) == 0: + basicConfig() + root.warning(msg, *args, **kwargs) + +def warn(msg, *args, **kwargs): + warnings.warn("The 'warn' function is deprecated, " + "use 'warning' instead", DeprecationWarning, 2) + warning(msg, *args, **kwargs) + +def info(msg, *args, **kwargs): + """ + Log a message with severity 'INFO' on the root logger. If the logger has + no handlers, call basicConfig() to add a console handler with a pre-defined + format. + """ + if len(root.handlers) == 0: + basicConfig() + root.info(msg, *args, **kwargs) + +def debug(msg, *args, **kwargs): + """ + Log a message with severity 'DEBUG' on the root logger. If the logger has + no handlers, call basicConfig() to add a console handler with a pre-defined + format. + """ + if len(root.handlers) == 0: + basicConfig() + root.debug(msg, *args, **kwargs) + +def log(level, msg, *args, **kwargs): + """ + Log 'msg % args' with the integer severity 'level' on the root logger. If + the logger has no handlers, call basicConfig() to add a console handler + with a pre-defined format. + """ + if len(root.handlers) == 0: + basicConfig() + root.log(level, msg, *args, **kwargs) + +def disable(level): + """ + Disable all logging calls of severity 'level' and below. + """ + root.manager.disable = level + +def shutdown(handlerList=_handlerList): + """ + Perform any cleanup actions in the logging system (e.g. flushing + buffers). + + Should be called at application exit. + """ + for wr in reversed(handlerList[:]): + #errors might occur, for example, if files are locked + #we just ignore them if raiseExceptions is not set + try: + h = wr() + if h: + try: + h.acquire() + h.flush() + h.close() + except (OSError, ValueError): + # Ignore errors which might be caused + # because handlers have been closed but + # references to them are still around at + # application exit. + pass + finally: + h.release() + except: # ignore everything, as we're shutting down + if raiseExceptions: + raise + #else, swallow + +#Let's try and shutdown automatically on application exit... +import atexit +atexit.register(shutdown) + +# Null handler + +class NullHandler(Handler): + """ + This handler does nothing. It's intended to be used to avoid the + "No handlers could be found for logger XXX" one-off warning. This is + important for library code, which may contain code to log events. If a user + of the library does not configure logging, the one-off warning might be + produced; to avoid this, the library developer simply needs to instantiate + a NullHandler and add it to the top-level logger of the library module or + package. + """ + def handle(self, record): + """Stub.""" + + def emit(self, record): + """Stub.""" + + def createLock(self): + self.lock = None + +# Warnings integration + +_warnings_showwarning = None + +def _showwarning(message, category, filename, lineno, file=None, line=None): + """ + Implementation of showwarnings which redirects to logging, which will first + check to see if the file parameter is None. If a file is specified, it will + delegate to the original warnings implementation of showwarning. Otherwise, + it will call warnings.formatwarning and will log the resulting string to a + warnings logger named "py.warnings" with level logging.WARNING. + """ + if file is not None: + if _warnings_showwarning is not None: + _warnings_showwarning(message, category, filename, lineno, file, line) + else: + s = warnings.formatwarning(message, category, filename, lineno, line) + logger = getLogger("py.warnings") + if not logger.handlers: + logger.addHandler(NullHandler()) + logger.warning("%s", s) + +def captureWarnings(capture): + """ + If capture is true, redirect all warnings to the logging package. + If capture is False, ensure that warnings are not redirected to logging + but to their original destinations. + """ + global _warnings_showwarning + if capture: + if _warnings_showwarning is None: + _warnings_showwarning = warnings.showwarning + warnings.showwarning = _showwarning + else: + if _warnings_showwarning is not None: + warnings.showwarning = _warnings_showwarning + _warnings_showwarning = None diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/logging/config.py b/src/image_occlusion_enhanced/libaddon/_vendor/logging/config.py new file mode 100644 index 00000000..a0d7d2ab --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/logging/config.py @@ -0,0 +1,944 @@ +# Copyright 2001-2014 by Vinay Sajip. All Rights Reserved. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose and without fee is hereby granted, +# provided that the above copyright notice appear in all copies and that +# both that copyright notice and this permission notice appear in +# supporting documentation, and that the name of Vinay Sajip +# not be used in advertising or publicity pertaining to distribution +# of the software without specific, written prior permission. +# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + + +""" +Configuration functions for the logging package for Python. The core package +is based on PEP 282 and comments thereto in comp.lang.python, and influenced +by Apache's log4j system. + +Copyright (C) 2001-2014 Vinay Sajip. All Rights Reserved. + +To use, simply 'import logging' and log away! +""" + +import errno +import io +from .. import logging +from .. import logging; from ..logging import handlers as ___vendorize__0; logging.handlers = ___vendorize__0 +import re +import struct +import sys +import traceback + +try: + import _thread as thread + import threading +except ImportError: #pragma: no cover + thread = None + +from socketserver import ThreadingTCPServer, StreamRequestHandler + + +DEFAULT_LOGGING_CONFIG_PORT = 9030 + +RESET_ERROR = errno.ECONNRESET + +# +# The following code implements a socket listener for on-the-fly +# reconfiguration of logging. +# +# _listener holds the server object doing the listening +_listener = None + +def fileConfig(fname, defaults=None, disable_existing_loggers=True): + """ + Read the logging configuration from a ConfigParser-format file. + + This can be called several times from an application, allowing an end user + the ability to select from various pre-canned configurations (if the + developer provides a mechanism to present the choices and load the chosen + configuration). + """ + import configparser + + if isinstance(fname, configparser.RawConfigParser): + cp = fname + else: + cp = configparser.ConfigParser(defaults) + if hasattr(fname, 'readline'): + cp.read_file(fname) + else: + cp.read(fname) + + formatters = _create_formatters(cp) + + # critical section + logging._acquireLock() + try: + _clearExistingHandlers() + + # Handlers add themselves to logging._handlers + handlers = _install_handlers(cp, formatters) + _install_loggers(cp, handlers, disable_existing_loggers) + finally: + logging._releaseLock() + + +def _resolve(name): + """Resolve a dotted name to a global object.""" + name = name.split('.') + used = name.pop(0) + found = __import__(used) + for n in name: + used = used + '.' + n + try: + found = getattr(found, n) + except AttributeError: + __import__(used) + found = getattr(found, n) + return found + +def _strip_spaces(alist): + return map(lambda x: x.strip(), alist) + +def _create_formatters(cp): + """Create and return formatters""" + flist = cp["formatters"]["keys"] + if not len(flist): + return {} + flist = flist.split(",") + flist = _strip_spaces(flist) + formatters = {} + for form in flist: + sectname = "formatter_%s" % form + fs = cp.get(sectname, "format", raw=True, fallback=None) + dfs = cp.get(sectname, "datefmt", raw=True, fallback=None) + stl = cp.get(sectname, "style", raw=True, fallback='%') + c = logging.Formatter + class_name = cp[sectname].get("class") + if class_name: + c = _resolve(class_name) + f = c(fs, dfs, stl) + formatters[form] = f + return formatters + + +def _install_handlers(cp, formatters): + """Install and return handlers""" + hlist = cp["handlers"]["keys"] + if not len(hlist): + return {} + hlist = hlist.split(",") + hlist = _strip_spaces(hlist) + handlers = {} + fixups = [] #for inter-handler references + for hand in hlist: + section = cp["handler_%s" % hand] + klass = section["class"] + fmt = section.get("formatter", "") + try: + klass = eval(klass, vars(logging)) + except (AttributeError, NameError): + klass = _resolve(klass) + args = section["args"] + args = eval(args, vars(logging)) + h = klass(*args) + if "level" in section: + level = section["level"] + h.setLevel(level) + if len(fmt): + h.setFormatter(formatters[fmt]) + if issubclass(klass, logging.handlers.MemoryHandler): + target = section.get("target", "") + if len(target): #the target handler may not be loaded yet, so keep for later... + fixups.append((h, target)) + handlers[hand] = h + #now all handlers are loaded, fixup inter-handler references... + for h, t in fixups: + h.setTarget(handlers[t]) + return handlers + +def _handle_existing_loggers(existing, child_loggers, disable_existing): + """ + When (re)configuring logging, handle loggers which were in the previous + configuration but are not in the new configuration. There's no point + deleting them as other threads may continue to hold references to them; + and by disabling them, you stop them doing any logging. + + However, don't disable children of named loggers, as that's probably not + what was intended by the user. Also, allow existing loggers to NOT be + disabled if disable_existing is false. + """ + root = logging.root + for log in existing: + logger = root.manager.loggerDict[log] + if log in child_loggers: + logger.level = logging.NOTSET + logger.handlers = [] + logger.propagate = True + else: + logger.disabled = disable_existing + +def _install_loggers(cp, handlers, disable_existing): + """Create and install loggers""" + + # configure the root first + llist = cp["loggers"]["keys"] + llist = llist.split(",") + llist = list(map(lambda x: x.strip(), llist)) + llist.remove("root") + section = cp["logger_root"] + root = logging.root + log = root + if "level" in section: + level = section["level"] + log.setLevel(level) + for h in root.handlers[:]: + root.removeHandler(h) + hlist = section["handlers"] + if len(hlist): + hlist = hlist.split(",") + hlist = _strip_spaces(hlist) + for hand in hlist: + log.addHandler(handlers[hand]) + + #and now the others... + #we don't want to lose the existing loggers, + #since other threads may have pointers to them. + #existing is set to contain all existing loggers, + #and as we go through the new configuration we + #remove any which are configured. At the end, + #what's left in existing is the set of loggers + #which were in the previous configuration but + #which are not in the new configuration. + existing = list(root.manager.loggerDict.keys()) + #The list needs to be sorted so that we can + #avoid disabling child loggers of explicitly + #named loggers. With a sorted list it is easier + #to find the child loggers. + existing.sort() + #We'll keep the list of existing loggers + #which are children of named loggers here... + child_loggers = [] + #now set up the new ones... + for log in llist: + section = cp["logger_%s" % log] + qn = section["qualname"] + propagate = section.getint("propagate", fallback=1) + logger = logging.getLogger(qn) + if qn in existing: + i = existing.index(qn) + 1 # start with the entry after qn + prefixed = qn + "." + pflen = len(prefixed) + num_existing = len(existing) + while i < num_existing: + if existing[i][:pflen] == prefixed: + child_loggers.append(existing[i]) + i += 1 + existing.remove(qn) + if "level" in section: + level = section["level"] + logger.setLevel(level) + for h in logger.handlers[:]: + logger.removeHandler(h) + logger.propagate = propagate + logger.disabled = 0 + hlist = section["handlers"] + if len(hlist): + hlist = hlist.split(",") + hlist = _strip_spaces(hlist) + for hand in hlist: + logger.addHandler(handlers[hand]) + + #Disable any old loggers. There's no point deleting + #them as other threads may continue to hold references + #and by disabling them, you stop them doing any logging. + #However, don't disable children of named loggers, as that's + #probably not what was intended by the user. + #for log in existing: + # logger = root.manager.loggerDict[log] + # if log in child_loggers: + # logger.level = logging.NOTSET + # logger.handlers = [] + # logger.propagate = 1 + # elif disable_existing_loggers: + # logger.disabled = 1 + _handle_existing_loggers(existing, child_loggers, disable_existing) + + +def _clearExistingHandlers(): + """Clear and close existing handlers""" + logging._handlers.clear() + logging.shutdown(logging._handlerList[:]) + del logging._handlerList[:] + + +IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I) + + +def valid_ident(s): + m = IDENTIFIER.match(s) + if not m: + raise ValueError('Not a valid Python identifier: %r' % s) + return True + + +class ConvertingMixin(object): + """For ConvertingXXX's, this mixin class provides common functions""" + + def convert_with_key(self, key, value, replace=True): + result = self.configurator.convert(value) + #If the converted value is different, save for next time + if value is not result: + if replace: + self[key] = result + if type(result) in (ConvertingDict, ConvertingList, + ConvertingTuple): + result.parent = self + result.key = key + return result + + def convert(self, value): + result = self.configurator.convert(value) + if value is not result: + if type(result) in (ConvertingDict, ConvertingList, + ConvertingTuple): + result.parent = self + return result + + +# The ConvertingXXX classes are wrappers around standard Python containers, +# and they serve to convert any suitable values in the container. The +# conversion converts base dicts, lists and tuples to their wrapped +# equivalents, whereas strings which match a conversion format are converted +# appropriately. +# +# Each wrapper should have a configurator attribute holding the actual +# configurator to use for conversion. + +class ConvertingDict(dict, ConvertingMixin): + """A converting dictionary wrapper.""" + + def __getitem__(self, key): + value = dict.__getitem__(self, key) + return self.convert_with_key(key, value) + + def get(self, key, default=None): + value = dict.get(self, key, default) + return self.convert_with_key(key, value) + + def pop(self, key, default=None): + value = dict.pop(self, key, default) + return self.convert_with_key(key, value, replace=False) + +class ConvertingList(list, ConvertingMixin): + """A converting list wrapper.""" + def __getitem__(self, key): + value = list.__getitem__(self, key) + return self.convert_with_key(key, value) + + def pop(self, idx=-1): + value = list.pop(self, idx) + return self.convert(value) + +class ConvertingTuple(tuple, ConvertingMixin): + """A converting tuple wrapper.""" + def __getitem__(self, key): + value = tuple.__getitem__(self, key) + # Can't replace a tuple entry. + return self.convert_with_key(key, value, replace=False) + +class BaseConfigurator(object): + """ + The configurator base class which defines some useful defaults. + """ + + CONVERT_PATTERN = re.compile(r'^(?P[a-z]+)://(?P.*)$') + + WORD_PATTERN = re.compile(r'^\s*(\w+)\s*') + DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*') + INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*') + DIGIT_PATTERN = re.compile(r'^\d+$') + + value_converters = { + 'ext' : 'ext_convert', + 'cfg' : 'cfg_convert', + } + + # We might want to use a different one, e.g. importlib + importer = staticmethod(__import__) + + def __init__(self, config): + self.config = ConvertingDict(config) + self.config.configurator = self + + def resolve(self, s): + """ + Resolve strings to objects using standard import and attribute + syntax. + """ + name = s.split('.') + used = name.pop(0) + try: + found = self.importer(used) + for frag in name: + used += '.' + frag + try: + found = getattr(found, frag) + except AttributeError: + self.importer(used) + found = getattr(found, frag) + return found + except ImportError: + e, tb = sys.exc_info()[1:] + v = ValueError('Cannot resolve %r: %s' % (s, e)) + v.__cause__, v.__traceback__ = e, tb + raise v + + def ext_convert(self, value): + """Default converter for the ext:// protocol.""" + return self.resolve(value) + + def cfg_convert(self, value): + """Default converter for the cfg:// protocol.""" + rest = value + m = self.WORD_PATTERN.match(rest) + if m is None: + raise ValueError("Unable to convert %r" % value) + else: + rest = rest[m.end():] + d = self.config[m.groups()[0]] + #print d, rest + while rest: + m = self.DOT_PATTERN.match(rest) + if m: + d = d[m.groups()[0]] + else: + m = self.INDEX_PATTERN.match(rest) + if m: + idx = m.groups()[0] + if not self.DIGIT_PATTERN.match(idx): + d = d[idx] + else: + try: + n = int(idx) # try as number first (most likely) + d = d[n] + except TypeError: + d = d[idx] + if m: + rest = rest[m.end():] + else: + raise ValueError('Unable to convert ' + '%r at %r' % (value, rest)) + #rest should be empty + return d + + def convert(self, value): + """ + Convert values to an appropriate type. dicts, lists and tuples are + replaced by their converting alternatives. Strings are checked to + see if they have a conversion format and are converted if they do. + """ + if not isinstance(value, ConvertingDict) and isinstance(value, dict): + value = ConvertingDict(value) + value.configurator = self + elif not isinstance(value, ConvertingList) and isinstance(value, list): + value = ConvertingList(value) + value.configurator = self + elif not isinstance(value, ConvertingTuple) and\ + isinstance(value, tuple): + value = ConvertingTuple(value) + value.configurator = self + elif isinstance(value, str): # str for py3k + m = self.CONVERT_PATTERN.match(value) + if m: + d = m.groupdict() + prefix = d['prefix'] + converter = self.value_converters.get(prefix, None) + if converter: + suffix = d['suffix'] + converter = getattr(self, converter) + value = converter(suffix) + return value + + def configure_custom(self, config): + """Configure an object with a user-supplied factory.""" + c = config.pop('()') + if not callable(c): + c = self.resolve(c) + props = config.pop('.', None) + # Check for valid identifiers + kwargs = dict([(k, config[k]) for k in config if valid_ident(k)]) + result = c(**kwargs) + if props: + for name, value in props.items(): + setattr(result, name, value) + return result + + def as_tuple(self, value): + """Utility function which converts lists to tuples.""" + if isinstance(value, list): + value = tuple(value) + return value + +class DictConfigurator(BaseConfigurator): + """ + Configure logging using a dictionary-like object to describe the + configuration. + """ + + def configure(self): + """Do the configuration.""" + + config = self.config + if 'version' not in config: + raise ValueError("dictionary doesn't specify a version") + if config['version'] != 1: + raise ValueError("Unsupported version: %s" % config['version']) + incremental = config.pop('incremental', False) + EMPTY_DICT = {} + logging._acquireLock() + try: + if incremental: + handlers = config.get('handlers', EMPTY_DICT) + for name in handlers: + if name not in logging._handlers: + raise ValueError('No handler found with ' + 'name %r' % name) + else: + try: + handler = logging._handlers[name] + handler_config = handlers[name] + level = handler_config.get('level', None) + if level: + handler.setLevel(logging._checkLevel(level)) + except Exception as e: + raise ValueError('Unable to configure handler ' + '%r: %s' % (name, e)) + loggers = config.get('loggers', EMPTY_DICT) + for name in loggers: + try: + self.configure_logger(name, loggers[name], True) + except Exception as e: + raise ValueError('Unable to configure logger ' + '%r: %s' % (name, e)) + root = config.get('root', None) + if root: + try: + self.configure_root(root, True) + except Exception as e: + raise ValueError('Unable to configure root ' + 'logger: %s' % e) + else: + disable_existing = config.pop('disable_existing_loggers', True) + + _clearExistingHandlers() + + # Do formatters first - they don't refer to anything else + formatters = config.get('formatters', EMPTY_DICT) + for name in formatters: + try: + formatters[name] = self.configure_formatter( + formatters[name]) + except Exception as e: + raise ValueError('Unable to configure ' + 'formatter %r: %s' % (name, e)) + # Next, do filters - they don't refer to anything else, either + filters = config.get('filters', EMPTY_DICT) + for name in filters: + try: + filters[name] = self.configure_filter(filters[name]) + except Exception as e: + raise ValueError('Unable to configure ' + 'filter %r: %s' % (name, e)) + + # Next, do handlers - they refer to formatters and filters + # As handlers can refer to other handlers, sort the keys + # to allow a deterministic order of configuration + handlers = config.get('handlers', EMPTY_DICT) + deferred = [] + for name in sorted(handlers): + try: + handler = self.configure_handler(handlers[name]) + handler.name = name + handlers[name] = handler + except Exception as e: + if 'target not configured yet' in str(e): + deferred.append(name) + else: + raise ValueError('Unable to configure handler ' + '%r: %s' % (name, e)) + + # Now do any that were deferred + for name in deferred: + try: + handler = self.configure_handler(handlers[name]) + handler.name = name + handlers[name] = handler + except Exception as e: + raise ValueError('Unable to configure handler ' + '%r: %s' % (name, e)) + + # Next, do loggers - they refer to handlers and filters + + #we don't want to lose the existing loggers, + #since other threads may have pointers to them. + #existing is set to contain all existing loggers, + #and as we go through the new configuration we + #remove any which are configured. At the end, + #what's left in existing is the set of loggers + #which were in the previous configuration but + #which are not in the new configuration. + root = logging.root + existing = list(root.manager.loggerDict.keys()) + #The list needs to be sorted so that we can + #avoid disabling child loggers of explicitly + #named loggers. With a sorted list it is easier + #to find the child loggers. + existing.sort() + #We'll keep the list of existing loggers + #which are children of named loggers here... + child_loggers = [] + #now set up the new ones... + loggers = config.get('loggers', EMPTY_DICT) + for name in loggers: + if name in existing: + i = existing.index(name) + 1 # look after name + prefixed = name + "." + pflen = len(prefixed) + num_existing = len(existing) + while i < num_existing: + if existing[i][:pflen] == prefixed: + child_loggers.append(existing[i]) + i += 1 + existing.remove(name) + try: + self.configure_logger(name, loggers[name]) + except Exception as e: + raise ValueError('Unable to configure logger ' + '%r: %s' % (name, e)) + + #Disable any old loggers. There's no point deleting + #them as other threads may continue to hold references + #and by disabling them, you stop them doing any logging. + #However, don't disable children of named loggers, as that's + #probably not what was intended by the user. + #for log in existing: + # logger = root.manager.loggerDict[log] + # if log in child_loggers: + # logger.level = logging.NOTSET + # logger.handlers = [] + # logger.propagate = True + # elif disable_existing: + # logger.disabled = True + _handle_existing_loggers(existing, child_loggers, + disable_existing) + + # And finally, do the root logger + root = config.get('root', None) + if root: + try: + self.configure_root(root) + except Exception as e: + raise ValueError('Unable to configure root ' + 'logger: %s' % e) + finally: + logging._releaseLock() + + def configure_formatter(self, config): + """Configure a formatter from a dictionary.""" + if '()' in config: + factory = config['()'] # for use in exception handler + try: + result = self.configure_custom(config) + except TypeError as te: + if "'format'" not in str(te): + raise + #Name of parameter changed from fmt to format. + #Retry with old name. + #This is so that code can be used with older Python versions + #(e.g. by Django) + config['fmt'] = config.pop('format') + config['()'] = factory + result = self.configure_custom(config) + else: + fmt = config.get('format', None) + dfmt = config.get('datefmt', None) + style = config.get('style', '%') + cname = config.get('class', None) + if not cname: + c = logging.Formatter + else: + c = _resolve(cname) + result = c(fmt, dfmt, style) + return result + + def configure_filter(self, config): + """Configure a filter from a dictionary.""" + if '()' in config: + result = self.configure_custom(config) + else: + name = config.get('name', '') + result = logging.Filter(name) + return result + + def add_filters(self, filterer, filters): + """Add filters to a filterer from a list of names.""" + for f in filters: + try: + filterer.addFilter(self.config['filters'][f]) + except Exception as e: + raise ValueError('Unable to add filter %r: %s' % (f, e)) + + def configure_handler(self, config): + """Configure a handler from a dictionary.""" + config_copy = dict(config) # for restoring in case of error + formatter = config.pop('formatter', None) + if formatter: + try: + formatter = self.config['formatters'][formatter] + except Exception as e: + raise ValueError('Unable to set formatter ' + '%r: %s' % (formatter, e)) + level = config.pop('level', None) + filters = config.pop('filters', None) + if '()' in config: + c = config.pop('()') + if not callable(c): + c = self.resolve(c) + factory = c + else: + cname = config.pop('class') + klass = self.resolve(cname) + #Special case for handler which refers to another handler + if issubclass(klass, logging.handlers.MemoryHandler) and\ + 'target' in config: + try: + th = self.config['handlers'][config['target']] + if not isinstance(th, logging.Handler): + config.update(config_copy) # restore for deferred cfg + raise TypeError('target not configured yet') + config['target'] = th + except Exception as e: + raise ValueError('Unable to set target handler ' + '%r: %s' % (config['target'], e)) + elif issubclass(klass, logging.handlers.SMTPHandler) and\ + 'mailhost' in config: + config['mailhost'] = self.as_tuple(config['mailhost']) + elif issubclass(klass, logging.handlers.SysLogHandler) and\ + 'address' in config: + config['address'] = self.as_tuple(config['address']) + factory = klass + props = config.pop('.', None) + kwargs = dict([(k, config[k]) for k in config if valid_ident(k)]) + try: + result = factory(**kwargs) + except TypeError as te: + if "'stream'" not in str(te): + raise + #The argument name changed from strm to stream + #Retry with old name. + #This is so that code can be used with older Python versions + #(e.g. by Django) + kwargs['strm'] = kwargs.pop('stream') + result = factory(**kwargs) + if formatter: + result.setFormatter(formatter) + if level is not None: + result.setLevel(logging._checkLevel(level)) + if filters: + self.add_filters(result, filters) + if props: + for name, value in props.items(): + setattr(result, name, value) + return result + + def add_handlers(self, logger, handlers): + """Add handlers to a logger from a list of names.""" + for h in handlers: + try: + logger.addHandler(self.config['handlers'][h]) + except Exception as e: + raise ValueError('Unable to add handler %r: %s' % (h, e)) + + def common_logger_config(self, logger, config, incremental=False): + """ + Perform configuration which is common to root and non-root loggers. + """ + level = config.get('level', None) + if level is not None: + logger.setLevel(logging._checkLevel(level)) + if not incremental: + #Remove any existing handlers + for h in logger.handlers[:]: + logger.removeHandler(h) + handlers = config.get('handlers', None) + if handlers: + self.add_handlers(logger, handlers) + filters = config.get('filters', None) + if filters: + self.add_filters(logger, filters) + + def configure_logger(self, name, config, incremental=False): + """Configure a non-root logger from a dictionary.""" + logger = logging.getLogger(name) + self.common_logger_config(logger, config, incremental) + propagate = config.get('propagate', None) + if propagate is not None: + logger.propagate = propagate + + def configure_root(self, config, incremental=False): + """Configure a root logger from a dictionary.""" + root = logging.getLogger() + self.common_logger_config(root, config, incremental) + +dictConfigClass = DictConfigurator + +def dictConfig(config): + """Configure logging using a dictionary.""" + dictConfigClass(config).configure() + + +def listen(port=DEFAULT_LOGGING_CONFIG_PORT, verify=None): + """ + Start up a socket server on the specified port, and listen for new + configurations. + + These will be sent as a file suitable for processing by fileConfig(). + Returns a Thread object on which you can call start() to start the server, + and which you can join() when appropriate. To stop the server, call + stopListening(). + + Use the ``verify`` argument to verify any bytes received across the wire + from a client. If specified, it should be a callable which receives a + single argument - the bytes of configuration data received across the + network - and it should return either ``None``, to indicate that the + passed in bytes could not be verified and should be discarded, or a + byte string which is then passed to the configuration machinery as + normal. Note that you can return transformed bytes, e.g. by decrypting + the bytes passed in. + """ + if not thread: #pragma: no cover + raise NotImplementedError("listen() needs threading to work") + + class ConfigStreamHandler(StreamRequestHandler): + """ + Handler for a logging configuration request. + + It expects a completely new logging configuration and uses fileConfig + to install it. + """ + def handle(self): + """ + Handle a request. + + Each request is expected to be a 4-byte length, packed using + struct.pack(">L", n), followed by the config file. + Uses fileConfig() to do the grunt work. + """ + try: + conn = self.connection + chunk = conn.recv(4) + if len(chunk) == 4: + slen = struct.unpack(">L", chunk)[0] + chunk = self.connection.recv(slen) + while len(chunk) < slen: + chunk = chunk + conn.recv(slen - len(chunk)) + if self.server.verify is not None: + chunk = self.server.verify(chunk) + if chunk is not None: # verified, can process + chunk = chunk.decode("utf-8") + try: + import json + d =json.loads(chunk) + assert isinstance(d, dict) + dictConfig(d) + except Exception: + #Apply new configuration. + + file = io.StringIO(chunk) + try: + fileConfig(file) + except Exception: + traceback.print_exc() + if self.server.ready: + self.server.ready.set() + except OSError as e: + if e.errno != RESET_ERROR: + raise + + class ConfigSocketReceiver(ThreadingTCPServer): + """ + A simple TCP socket-based logging config receiver. + """ + + allow_reuse_address = 1 + + def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT, + handler=None, ready=None, verify=None): + ThreadingTCPServer.__init__(self, (host, port), handler) + logging._acquireLock() + self.abort = 0 + logging._releaseLock() + self.timeout = 1 + self.ready = ready + self.verify = verify + + def serve_until_stopped(self): + import select + abort = 0 + while not abort: + rd, wr, ex = select.select([self.socket.fileno()], + [], [], + self.timeout) + if rd: + self.handle_request() + logging._acquireLock() + abort = self.abort + logging._releaseLock() + self.socket.close() + + class Server(threading.Thread): + + def __init__(self, rcvr, hdlr, port, verify): + super(Server, self).__init__() + self.rcvr = rcvr + self.hdlr = hdlr + self.port = port + self.verify = verify + self.ready = threading.Event() + + def run(self): + server = self.rcvr(port=self.port, handler=self.hdlr, + ready=self.ready, + verify=self.verify) + if self.port == 0: + self.port = server.server_address[1] + self.ready.set() + global _listener + logging._acquireLock() + _listener = server + logging._releaseLock() + server.serve_until_stopped() + + return Server(ConfigSocketReceiver, ConfigStreamHandler, port, verify) + +def stopListening(): + """ + Stop the listening server which was created with a call to listen(). + """ + global _listener + logging._acquireLock() + try: + if _listener: + _listener.abort = 1 + _listener = None + finally: + logging._releaseLock() \ No newline at end of file diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/logging/handlers.py b/src/image_occlusion_enhanced/libaddon/_vendor/logging/handlers.py new file mode 100644 index 00000000..0851a23a --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/logging/handlers.py @@ -0,0 +1,1509 @@ +# Copyright 2001-2016 by Vinay Sajip. All Rights Reserved. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose and without fee is hereby granted, +# provided that the above copyright notice appear in all copies and that +# both that copyright notice and this permission notice appear in +# supporting documentation, and that the name of Vinay Sajip +# not be used in advertising or publicity pertaining to distribution +# of the software without specific, written prior permission. +# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + + +""" +Additional handlers for the logging package for Python. The core package is +based on PEP 282 and comments thereto in comp.lang.python. + +Copyright (C) 2001-2016 Vinay Sajip. All Rights Reserved. + +To use, simply 'import logging.handlers' and log away! +""" + +from .. import logging; import socket; import os; import pickle; import struct; import time; import re +from stat import ST_DEV, ST_INO, ST_MTIME +import queue +try: + import threading +except ImportError: #pragma: no cover + threading = None + +# +# Some constants... +# + +DEFAULT_TCP_LOGGING_PORT = 9020 +DEFAULT_UDP_LOGGING_PORT = 9021 +DEFAULT_HTTP_LOGGING_PORT = 9022 +DEFAULT_SOAP_LOGGING_PORT = 9023 +SYSLOG_UDP_PORT = 514 +SYSLOG_TCP_PORT = 514 + +_MIDNIGHT = 24 * 60 * 60 # number of seconds in a day + +class BaseRotatingHandler(logging.FileHandler): + """ + Base class for handlers that rotate log files at a certain point. + Not meant to be instantiated directly. Instead, use RotatingFileHandler + or TimedRotatingFileHandler. + """ + def __init__(self, filename, mode, encoding=None, delay=False): + """ + Use the specified filename for streamed logging + """ + logging.FileHandler.__init__(self, filename, mode, encoding, delay) + self.mode = mode + self.encoding = encoding + self.namer = None + self.rotator = None + + def emit(self, record): + """ + Emit a record. + + Output the record to the file, catering for rollover as described + in doRollover(). + """ + try: + if self.shouldRollover(record): + self.doRollover() + logging.FileHandler.emit(self, record) + except Exception: + self.handleError(record) + + def rotation_filename(self, default_name): + """ + Modify the filename of a log file when rotating. + + This is provided so that a custom filename can be provided. + + The default implementation calls the 'namer' attribute of the + handler, if it's callable, passing the default name to + it. If the attribute isn't callable (the default is None), the name + is returned unchanged. + + :param default_name: The default name for the log file. + """ + if not callable(self.namer): + result = default_name + else: + result = self.namer(default_name) + return result + + def rotate(self, source, dest): + """ + When rotating, rotate the current log. + + The default implementation calls the 'rotator' attribute of the + handler, if it's callable, passing the source and dest arguments to + it. If the attribute isn't callable (the default is None), the source + is simply renamed to the destination. + + :param source: The source filename. This is normally the base + filename, e.g. 'test.log' + :param dest: The destination filename. This is normally + what the source is rotated to, e.g. 'test.log.1'. + """ + if not callable(self.rotator): + # Issue 18940: A file may not have been created if delay is True. + if os.path.exists(source): + os.rename(source, dest) + else: + self.rotator(source, dest) + +class RotatingFileHandler(BaseRotatingHandler): + """ + Handler for logging to a set of files, which switches from one file + to the next when the current file reaches a certain size. + """ + def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False): + """ + Open the specified file and use it as the stream for logging. + + By default, the file grows indefinitely. You can specify particular + values of maxBytes and backupCount to allow the file to rollover at + a predetermined size. + + Rollover occurs whenever the current log file is nearly maxBytes in + length. If backupCount is >= 1, the system will successively create + new files with the same pathname as the base file, but with extensions + ".1", ".2" etc. appended to it. For example, with a backupCount of 5 + and a base file name of "app.log", you would get "app.log", + "app.log.1", "app.log.2", ... through to "app.log.5". The file being + written to is always "app.log" - when it gets filled up, it is closed + and renamed to "app.log.1", and if files "app.log.1", "app.log.2" etc. + exist, then they are renamed to "app.log.2", "app.log.3" etc. + respectively. + + If maxBytes is zero, rollover never occurs. + """ + # If rotation/rollover is wanted, it doesn't make sense to use another + # mode. If for example 'w' were specified, then if there were multiple + # runs of the calling application, the logs from previous runs would be + # lost if the 'w' is respected, because the log file would be truncated + # on each run. + if maxBytes > 0: + mode = 'a' + BaseRotatingHandler.__init__(self, filename, mode, encoding, delay) + self.maxBytes = maxBytes + self.backupCount = backupCount + + def doRollover(self): + """ + Do a rollover, as described in __init__(). + """ + if self.stream: + self.stream.close() + self.stream = None + if self.backupCount > 0: + for i in range(self.backupCount - 1, 0, -1): + sfn = self.rotation_filename("%s.%d" % (self.baseFilename, i)) + dfn = self.rotation_filename("%s.%d" % (self.baseFilename, + i + 1)) + if os.path.exists(sfn): + if os.path.exists(dfn): + os.remove(dfn) + os.rename(sfn, dfn) + dfn = self.rotation_filename(self.baseFilename + ".1") + if os.path.exists(dfn): + os.remove(dfn) + self.rotate(self.baseFilename, dfn) + if not self.delay: + self.stream = self._open() + + def shouldRollover(self, record): + """ + Determine if rollover should occur. + + Basically, see if the supplied record would cause the file to exceed + the size limit we have. + """ + if self.stream is None: # delay was set... + self.stream = self._open() + if self.maxBytes > 0: # are we rolling over? + msg = "%s\n" % self.format(record) + self.stream.seek(0, 2) #due to non-posix-compliant Windows feature + if self.stream.tell() + len(msg) >= self.maxBytes: + return 1 + return 0 + +class TimedRotatingFileHandler(BaseRotatingHandler): + """ + Handler for logging to a file, rotating the log file at certain timed + intervals. + + If backupCount is > 0, when rollover is done, no more than backupCount + files are kept - the oldest ones are deleted. + """ + def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None): + BaseRotatingHandler.__init__(self, filename, 'a', encoding, delay) + self.when = when.upper() + self.backupCount = backupCount + self.utc = utc + self.atTime = atTime + # Calculate the real rollover interval, which is just the number of + # seconds between rollovers. Also set the filename suffix used when + # a rollover occurs. Current 'when' events supported: + # S - Seconds + # M - Minutes + # H - Hours + # D - Days + # midnight - roll over at midnight + # W{0-6} - roll over on a certain day; 0 - Monday + # + # Case of the 'when' specifier is not important; lower or upper case + # will work. + if self.when == 'S': + self.interval = 1 # one second + self.suffix = "%Y-%m-%d_%H-%M-%S" + self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$" + elif self.when == 'M': + self.interval = 60 # one minute + self.suffix = "%Y-%m-%d_%H-%M" + self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}(\.\w+)?$" + elif self.when == 'H': + self.interval = 60 * 60 # one hour + self.suffix = "%Y-%m-%d_%H" + self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}(\.\w+)?$" + elif self.when == 'D' or self.when == 'MIDNIGHT': + self.interval = 60 * 60 * 24 # one day + self.suffix = "%Y-%m-%d" + self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$" + elif self.when.startswith('W'): + self.interval = 60 * 60 * 24 * 7 # one week + if len(self.when) != 2: + raise ValueError("You must specify a day for weekly rollover from 0 to 6 (0 is Monday): %s" % self.when) + if self.when[1] < '0' or self.when[1] > '6': + raise ValueError("Invalid day specified for weekly rollover: %s" % self.when) + self.dayOfWeek = int(self.when[1]) + self.suffix = "%Y-%m-%d" + self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$" + else: + raise ValueError("Invalid rollover interval specified: %s" % self.when) + + self.extMatch = re.compile(self.extMatch, re.ASCII) + self.interval = self.interval * interval # multiply by units requested + # The following line added because the filename passed in could be a + # path object (see Issue #27493), but self.baseFilename will be a string + filename = self.baseFilename + if os.path.exists(filename): + t = os.stat(filename)[ST_MTIME] + else: + t = int(time.time()) + self.rolloverAt = self.computeRollover(t) + + def computeRollover(self, currentTime): + """ + Work out the rollover time based on the specified time. + """ + result = currentTime + self.interval + # If we are rolling over at midnight or weekly, then the interval is already known. + # What we need to figure out is WHEN the next interval is. In other words, + # if you are rolling over at midnight, then your base interval is 1 day, + # but you want to start that one day clock at midnight, not now. So, we + # have to fudge the rolloverAt value in order to trigger the first rollover + # at the right time. After that, the regular interval will take care of + # the rest. Note that this code doesn't care about leap seconds. :) + if self.when == 'MIDNIGHT' or self.when.startswith('W'): + # This could be done with less code, but I wanted it to be clear + if self.utc: + t = time.gmtime(currentTime) + else: + t = time.localtime(currentTime) + currentHour = t[3] + currentMinute = t[4] + currentSecond = t[5] + currentDay = t[6] + # r is the number of seconds left between now and the next rotation + if self.atTime is None: + rotate_ts = _MIDNIGHT + else: + rotate_ts = ((self.atTime.hour * 60 + self.atTime.minute)*60 + + self.atTime.second) + + r = rotate_ts - ((currentHour * 60 + currentMinute) * 60 + + currentSecond) + if r < 0: + # Rotate time is before the current time (for example when + # self.rotateAt is 13:45 and it now 14:15), rotation is + # tomorrow. + r += _MIDNIGHT + currentDay = (currentDay + 1) % 7 + result = currentTime + r + # If we are rolling over on a certain day, add in the number of days until + # the next rollover, but offset by 1 since we just calculated the time + # until the next day starts. There are three cases: + # Case 1) The day to rollover is today; in this case, do nothing + # Case 2) The day to rollover is further in the interval (i.e., today is + # day 2 (Wednesday) and rollover is on day 6 (Sunday). Days to + # next rollover is simply 6 - 2 - 1, or 3. + # Case 3) The day to rollover is behind us in the interval (i.e., today + # is day 5 (Saturday) and rollover is on day 3 (Thursday). + # Days to rollover is 6 - 5 + 3, or 4. In this case, it's the + # number of days left in the current week (1) plus the number + # of days in the next week until the rollover day (3). + # The calculations described in 2) and 3) above need to have a day added. + # This is because the above time calculation takes us to midnight on this + # day, i.e. the start of the next day. + if self.when.startswith('W'): + day = currentDay # 0 is Monday + if day != self.dayOfWeek: + if day < self.dayOfWeek: + daysToWait = self.dayOfWeek - day + else: + daysToWait = 6 - day + self.dayOfWeek + 1 + newRolloverAt = result + (daysToWait * (60 * 60 * 24)) + if not self.utc: + dstNow = t[-1] + dstAtRollover = time.localtime(newRolloverAt)[-1] + if dstNow != dstAtRollover: + if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour + addend = -3600 + else: # DST bows out before next rollover, so we need to add an hour + addend = 3600 + newRolloverAt += addend + result = newRolloverAt + return result + + def shouldRollover(self, record): + """ + Determine if rollover should occur. + + record is not used, as we are just comparing times, but it is needed so + the method signatures are the same + """ + t = int(time.time()) + if t >= self.rolloverAt: + return 1 + return 0 + + def getFilesToDelete(self): + """ + Determine the files to delete when rolling over. + + More specific than the earlier method, which just used glob.glob(). + """ + dirName, baseName = os.path.split(self.baseFilename) + fileNames = os.listdir(dirName) + result = [] + prefix = baseName + "." + plen = len(prefix) + for fileName in fileNames: + if fileName[:plen] == prefix: + suffix = fileName[plen:] + if self.extMatch.match(suffix): + result.append(os.path.join(dirName, fileName)) + if len(result) < self.backupCount: + result = [] + else: + result.sort() + result = result[:len(result) - self.backupCount] + return result + + def doRollover(self): + """ + do a rollover; in this case, a date/time stamp is appended to the filename + when the rollover happens. However, you want the file to be named for the + start of the interval, not the current time. If there is a backup count, + then we have to get a list of matching filenames, sort them and remove + the one with the oldest suffix. + """ + if self.stream: + self.stream.close() + self.stream = None + # get the time that this sequence started at and make it a TimeTuple + currentTime = int(time.time()) + dstNow = time.localtime(currentTime)[-1] + t = self.rolloverAt - self.interval + if self.utc: + timeTuple = time.gmtime(t) + else: + timeTuple = time.localtime(t) + dstThen = timeTuple[-1] + if dstNow != dstThen: + if dstNow: + addend = 3600 + else: + addend = -3600 + timeTuple = time.localtime(t + addend) + dfn = self.rotation_filename(self.baseFilename + "." + + time.strftime(self.suffix, timeTuple)) + if os.path.exists(dfn): + os.remove(dfn) + self.rotate(self.baseFilename, dfn) + if self.backupCount > 0: + for s in self.getFilesToDelete(): + os.remove(s) + if not self.delay: + self.stream = self._open() + newRolloverAt = self.computeRollover(currentTime) + while newRolloverAt <= currentTime: + newRolloverAt = newRolloverAt + self.interval + #If DST changes and midnight or weekly rollover, adjust for this. + if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc: + dstAtRollover = time.localtime(newRolloverAt)[-1] + if dstNow != dstAtRollover: + if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour + addend = -3600 + else: # DST bows out before next rollover, so we need to add an hour + addend = 3600 + newRolloverAt += addend + self.rolloverAt = newRolloverAt + +class WatchedFileHandler(logging.FileHandler): + """ + A handler for logging to a file, which watches the file + to see if it has changed while in use. This can happen because of + usage of programs such as newsyslog and logrotate which perform + log file rotation. This handler, intended for use under Unix, + watches the file to see if it has changed since the last emit. + (A file has changed if its device or inode have changed.) + If it has changed, the old file stream is closed, and the file + opened to get a new stream. + + This handler is not appropriate for use under Windows, because + under Windows open files cannot be moved or renamed - logging + opens the files with exclusive locks - and so there is no need + for such a handler. Furthermore, ST_INO is not supported under + Windows; stat always returns zero for this value. + + This handler is based on a suggestion and patch by Chad J. + Schroeder. + """ + def __init__(self, filename, mode='a', encoding=None, delay=False): + logging.FileHandler.__init__(self, filename, mode, encoding, delay) + self.dev, self.ino = -1, -1 + self._statstream() + + def _statstream(self): + if self.stream: + sres = os.fstat(self.stream.fileno()) + self.dev, self.ino = sres[ST_DEV], sres[ST_INO] + + def reopenIfNeeded(self): + """ + Reopen log file if needed. + + Checks if the underlying file has changed, and if it + has, close the old stream and reopen the file to get the + current stream. + """ + # Reduce the chance of race conditions by stat'ing by path only + # once and then fstat'ing our new fd if we opened a new log stream. + # See issue #14632: Thanks to John Mulligan for the problem report + # and patch. + try: + # stat the file by path, checking for existence + sres = os.stat(self.baseFilename) + except FileNotFoundError: + sres = None + # compare file system stat with that of our stream file handle + if not sres or sres[ST_DEV] != self.dev or sres[ST_INO] != self.ino: + if self.stream is not None: + # we have an open file handle, clean it up + self.stream.flush() + self.stream.close() + self.stream = None # See Issue #21742: _open () might fail. + # open a new file handle and get new stat info from that fd + self.stream = self._open() + self._statstream() + + def emit(self, record): + """ + Emit a record. + + If underlying file has changed, reopen the file before emitting the + record to it. + """ + self.reopenIfNeeded() + logging.FileHandler.emit(self, record) + + +class SocketHandler(logging.Handler): + """ + A handler class which writes logging records, in pickle format, to + a streaming socket. The socket is kept open across logging calls. + If the peer resets it, an attempt is made to reconnect on the next call. + The pickle which is sent is that of the LogRecord's attribute dictionary + (__dict__), so that the receiver does not need to have the logging module + installed in order to process the logging event. + + To unpickle the record at the receiving end into a LogRecord, use the + makeLogRecord function. + """ + + def __init__(self, host, port): + """ + Initializes the handler with a specific host address and port. + + When the attribute *closeOnError* is set to True - if a socket error + occurs, the socket is silently closed and then reopened on the next + logging call. + """ + logging.Handler.__init__(self) + self.host = host + self.port = port + if port is None: + self.address = host + else: + self.address = (host, port) + self.sock = None + self.closeOnError = False + self.retryTime = None + # + # Exponential backoff parameters. + # + self.retryStart = 1.0 + self.retryMax = 30.0 + self.retryFactor = 2.0 + + def makeSocket(self, timeout=1): + """ + A factory method which allows subclasses to define the precise + type of socket they want. + """ + if self.port is not None: + result = socket.create_connection(self.address, timeout=timeout) + else: + result = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + result.settimeout(timeout) + try: + result.connect(self.address) + except OSError: + result.close() # Issue 19182 + raise + return result + + def createSocket(self): + """ + Try to create a socket, using an exponential backoff with + a max retry time. Thanks to Robert Olson for the original patch + (SF #815911) which has been slightly refactored. + """ + now = time.time() + # Either retryTime is None, in which case this + # is the first time back after a disconnect, or + # we've waited long enough. + if self.retryTime is None: + attempt = True + else: + attempt = (now >= self.retryTime) + if attempt: + try: + self.sock = self.makeSocket() + self.retryTime = None # next time, no delay before trying + except OSError: + #Creation failed, so set the retry time and return. + if self.retryTime is None: + self.retryPeriod = self.retryStart + else: + self.retryPeriod = self.retryPeriod * self.retryFactor + if self.retryPeriod > self.retryMax: + self.retryPeriod = self.retryMax + self.retryTime = now + self.retryPeriod + + def send(self, s): + """ + Send a pickled string to the socket. + + This function allows for partial sends which can happen when the + network is busy. + """ + if self.sock is None: + self.createSocket() + #self.sock can be None either because we haven't reached the retry + #time yet, or because we have reached the retry time and retried, + #but are still unable to connect. + if self.sock: + try: + self.sock.sendall(s) + except OSError: #pragma: no cover + self.sock.close() + self.sock = None # so we can call createSocket next time + + def makePickle(self, record): + """ + Pickles the record in binary format with a length prefix, and + returns it ready for transmission across the socket. + """ + ei = record.exc_info + if ei: + # just to get traceback text into record.exc_text ... + dummy = self.format(record) + # See issue #14436: If msg or args are objects, they may not be + # available on the receiving end. So we convert the msg % args + # to a string, save it as msg and zap the args. + d = dict(record.__dict__) + d['msg'] = record.getMessage() + d['args'] = None + d['exc_info'] = None + # Issue #25685: delete 'message' if present: redundant with 'msg' + d.pop('message', None) + s = pickle.dumps(d, 1) + slen = struct.pack(">L", len(s)) + return slen + s + + def handleError(self, record): + """ + Handle an error during logging. + + An error has occurred during logging. Most likely cause - + connection lost. Close the socket so that we can retry on the + next event. + """ + if self.closeOnError and self.sock: + self.sock.close() + self.sock = None #try to reconnect next time + else: + logging.Handler.handleError(self, record) + + def emit(self, record): + """ + Emit a record. + + Pickles the record and writes it to the socket in binary format. + If there is an error with the socket, silently drop the packet. + If there was a problem with the socket, re-establishes the + socket. + """ + try: + s = self.makePickle(record) + self.send(s) + except Exception: + self.handleError(record) + + def close(self): + """ + Closes the socket. + """ + self.acquire() + try: + sock = self.sock + if sock: + self.sock = None + sock.close() + logging.Handler.close(self) + finally: + self.release() + +class DatagramHandler(SocketHandler): + """ + A handler class which writes logging records, in pickle format, to + a datagram socket. The pickle which is sent is that of the LogRecord's + attribute dictionary (__dict__), so that the receiver does not need to + have the logging module installed in order to process the logging event. + + To unpickle the record at the receiving end into a LogRecord, use the + makeLogRecord function. + + """ + def __init__(self, host, port): + """ + Initializes the handler with a specific host address and port. + """ + SocketHandler.__init__(self, host, port) + self.closeOnError = False + + def makeSocket(self): + """ + The factory method of SocketHandler is here overridden to create + a UDP socket (SOCK_DGRAM). + """ + if self.port is None: + family = socket.AF_UNIX + else: + family = socket.AF_INET + s = socket.socket(family, socket.SOCK_DGRAM) + return s + + def send(self, s): + """ + Send a pickled string to a socket. + + This function no longer allows for partial sends which can happen + when the network is busy - UDP does not guarantee delivery and + can deliver packets out of sequence. + """ + if self.sock is None: + self.createSocket() + self.sock.sendto(s, self.address) + +class SysLogHandler(logging.Handler): + """ + A handler class which sends formatted logging records to a syslog + server. Based on Sam Rushing's syslog module: + http://www.nightmare.com/squirl/python-ext/misc/syslog.py + Contributed by Nicolas Untz (after which minor refactoring changes + have been made). + """ + + # from : + # ====================================================================== + # priorities/facilities are encoded into a single 32-bit quantity, where + # the bottom 3 bits are the priority (0-7) and the top 28 bits are the + # facility (0-big number). Both the priorities and the facilities map + # roughly one-to-one to strings in the syslogd(8) source code. This + # mapping is included in this file. + # + # priorities (these are ordered) + + LOG_EMERG = 0 # system is unusable + LOG_ALERT = 1 # action must be taken immediately + LOG_CRIT = 2 # critical conditions + LOG_ERR = 3 # error conditions + LOG_WARNING = 4 # warning conditions + LOG_NOTICE = 5 # normal but significant condition + LOG_INFO = 6 # informational + LOG_DEBUG = 7 # debug-level messages + + # facility codes + LOG_KERN = 0 # kernel messages + LOG_USER = 1 # random user-level messages + LOG_MAIL = 2 # mail system + LOG_DAEMON = 3 # system daemons + LOG_AUTH = 4 # security/authorization messages + LOG_SYSLOG = 5 # messages generated internally by syslogd + LOG_LPR = 6 # line printer subsystem + LOG_NEWS = 7 # network news subsystem + LOG_UUCP = 8 # UUCP subsystem + LOG_CRON = 9 # clock daemon + LOG_AUTHPRIV = 10 # security/authorization messages (private) + LOG_FTP = 11 # FTP daemon + + # other codes through 15 reserved for system use + LOG_LOCAL0 = 16 # reserved for local use + LOG_LOCAL1 = 17 # reserved for local use + LOG_LOCAL2 = 18 # reserved for local use + LOG_LOCAL3 = 19 # reserved for local use + LOG_LOCAL4 = 20 # reserved for local use + LOG_LOCAL5 = 21 # reserved for local use + LOG_LOCAL6 = 22 # reserved for local use + LOG_LOCAL7 = 23 # reserved for local use + + priority_names = { + "alert": LOG_ALERT, + "crit": LOG_CRIT, + "critical": LOG_CRIT, + "debug": LOG_DEBUG, + "emerg": LOG_EMERG, + "err": LOG_ERR, + "error": LOG_ERR, # DEPRECATED + "info": LOG_INFO, + "notice": LOG_NOTICE, + "panic": LOG_EMERG, # DEPRECATED + "warn": LOG_WARNING, # DEPRECATED + "warning": LOG_WARNING, + } + + facility_names = { + "auth": LOG_AUTH, + "authpriv": LOG_AUTHPRIV, + "cron": LOG_CRON, + "daemon": LOG_DAEMON, + "ftp": LOG_FTP, + "kern": LOG_KERN, + "lpr": LOG_LPR, + "mail": LOG_MAIL, + "news": LOG_NEWS, + "security": LOG_AUTH, # DEPRECATED + "syslog": LOG_SYSLOG, + "user": LOG_USER, + "uucp": LOG_UUCP, + "local0": LOG_LOCAL0, + "local1": LOG_LOCAL1, + "local2": LOG_LOCAL2, + "local3": LOG_LOCAL3, + "local4": LOG_LOCAL4, + "local5": LOG_LOCAL5, + "local6": LOG_LOCAL6, + "local7": LOG_LOCAL7, + } + + #The map below appears to be trivially lowercasing the key. However, + #there's more to it than meets the eye - in some locales, lowercasing + #gives unexpected results. See SF #1524081: in the Turkish locale, + #"INFO".lower() != "info" + priority_map = { + "DEBUG" : "debug", + "INFO" : "info", + "WARNING" : "warning", + "ERROR" : "error", + "CRITICAL" : "critical" + } + + def __init__(self, address=('localhost', SYSLOG_UDP_PORT), + facility=LOG_USER, socktype=None): + """ + Initialize a handler. + + If address is specified as a string, a UNIX socket is used. To log to a + local syslogd, "SysLogHandler(address="/dev/log")" can be used. + If facility is not specified, LOG_USER is used. If socktype is + specified as socket.SOCK_DGRAM or socket.SOCK_STREAM, that specific + socket type will be used. For Unix sockets, you can also specify a + socktype of None, in which case socket.SOCK_DGRAM will be used, falling + back to socket.SOCK_STREAM. + """ + logging.Handler.__init__(self) + + self.address = address + self.facility = facility + self.socktype = socktype + + if isinstance(address, str): + self.unixsocket = True + # Syslog server may be unavailable during handler initialisation. + # C's openlog() function also ignores connection errors. + # Moreover, we ignore these errors while logging, so it not worse + # to ignore it also here. + try: + self._connect_unixsocket(address) + except OSError: + pass + else: + self.unixsocket = False + if socktype is None: + socktype = socket.SOCK_DGRAM + host, port = address + ress = socket.getaddrinfo(host, port, 0, socktype) + if not ress: + raise OSError("getaddrinfo returns an empty list") + for res in ress: + af, socktype, proto, _, sa = res + err = sock = None + try: + sock = socket.socket(af, socktype, proto) + if socktype == socket.SOCK_STREAM: + sock.connect(sa) + break + except OSError as exc: + err = exc + if sock is not None: + sock.close() + if err is not None: + raise err + self.socket = sock + self.socktype = socktype + + def _connect_unixsocket(self, address): + use_socktype = self.socktype + if use_socktype is None: + use_socktype = socket.SOCK_DGRAM + self.socket = socket.socket(socket.AF_UNIX, use_socktype) + try: + self.socket.connect(address) + # it worked, so set self.socktype to the used type + self.socktype = use_socktype + except OSError: + self.socket.close() + if self.socktype is not None: + # user didn't specify falling back, so fail + raise + use_socktype = socket.SOCK_STREAM + self.socket = socket.socket(socket.AF_UNIX, use_socktype) + try: + self.socket.connect(address) + # it worked, so set self.socktype to the used type + self.socktype = use_socktype + except OSError: + self.socket.close() + raise + + def encodePriority(self, facility, priority): + """ + Encode the facility and priority. You can pass in strings or + integers - if strings are passed, the facility_names and + priority_names mapping dictionaries are used to convert them to + integers. + """ + if isinstance(facility, str): + facility = self.facility_names[facility] + if isinstance(priority, str): + priority = self.priority_names[priority] + return (facility << 3) | priority + + def close(self): + """ + Closes the socket. + """ + self.acquire() + try: + self.socket.close() + logging.Handler.close(self) + finally: + self.release() + + def mapPriority(self, levelName): + """ + Map a logging level name to a key in the priority_names map. + This is useful in two scenarios: when custom levels are being + used, and in the case where you can't do a straightforward + mapping by lowercasing the logging level name because of locale- + specific issues (see SF #1524081). + """ + return self.priority_map.get(levelName, "warning") + + ident = '' # prepended to all messages + append_nul = True # some old syslog daemons expect a NUL terminator + + def emit(self, record): + """ + Emit a record. + + The record is formatted, and then sent to the syslog server. If + exception information is present, it is NOT sent to the server. + """ + try: + msg = self.format(record) + if self.ident: + msg = self.ident + msg + if self.append_nul: + msg += '\000' + + # We need to convert record level to lowercase, maybe this will + # change in the future. + prio = '<%d>' % self.encodePriority(self.facility, + self.mapPriority(record.levelname)) + prio = prio.encode('utf-8') + # Message is a string. Convert to bytes as required by RFC 5424 + msg = msg.encode('utf-8') + msg = prio + msg + if self.unixsocket: + try: + self.socket.send(msg) + except OSError: + self.socket.close() + self._connect_unixsocket(self.address) + self.socket.send(msg) + elif self.socktype == socket.SOCK_DGRAM: + self.socket.sendto(msg, self.address) + else: + self.socket.sendall(msg) + except Exception: + self.handleError(record) + +class SMTPHandler(logging.Handler): + """ + A handler class which sends an SMTP email for each logging event. + """ + def __init__(self, mailhost, fromaddr, toaddrs, subject, + credentials=None, secure=None, timeout=5.0): + """ + Initialize the handler. + + Initialize the instance with the from and to addresses and subject + line of the email. To specify a non-standard SMTP port, use the + (host, port) tuple format for the mailhost argument. To specify + authentication credentials, supply a (username, password) tuple + for the credentials argument. To specify the use of a secure + protocol (TLS), pass in a tuple for the secure argument. This will + only be used when authentication credentials are supplied. The tuple + will be either an empty tuple, or a single-value tuple with the name + of a keyfile, or a 2-value tuple with the names of the keyfile and + certificate file. (This tuple is passed to the `starttls` method). + A timeout in seconds can be specified for the SMTP connection (the + default is one second). + """ + logging.Handler.__init__(self) + if isinstance(mailhost, (list, tuple)): + self.mailhost, self.mailport = mailhost + else: + self.mailhost, self.mailport = mailhost, None + if isinstance(credentials, (list, tuple)): + self.username, self.password = credentials + else: + self.username = None + self.fromaddr = fromaddr + if isinstance(toaddrs, str): + toaddrs = [toaddrs] + self.toaddrs = toaddrs + self.subject = subject + self.secure = secure + self.timeout = timeout + + def getSubject(self, record): + """ + Determine the subject for the email. + + If you want to specify a subject line which is record-dependent, + override this method. + """ + return self.subject + + def emit(self, record): + """ + Emit a record. + + Format the record and send it to the specified addressees. + """ + try: + import smtplib + from email.message import EmailMessage + import email.utils + + port = self.mailport + if not port: + port = smtplib.SMTP_PORT + smtp = smtplib.SMTP(self.mailhost, port, timeout=self.timeout) + msg = EmailMessage() + msg['From'] = self.fromaddr + msg['To'] = ','.join(self.toaddrs) + msg['Subject'] = self.getSubject(record) + msg['Date'] = email.utils.localtime() + msg.set_content(self.format(record)) + if self.username: + if self.secure is not None: + smtp.ehlo() + smtp.starttls(*self.secure) + smtp.ehlo() + smtp.login(self.username, self.password) + smtp.send_message(msg) + smtp.quit() + except Exception: + self.handleError(record) + +class NTEventLogHandler(logging.Handler): + """ + A handler class which sends events to the NT Event Log. Adds a + registry entry for the specified application name. If no dllname is + provided, win32service.pyd (which contains some basic message + placeholders) is used. Note that use of these placeholders will make + your event logs big, as the entire message source is held in the log. + If you want slimmer logs, you have to pass in the name of your own DLL + which contains the message definitions you want to use in the event log. + """ + def __init__(self, appname, dllname=None, logtype="Application"): + logging.Handler.__init__(self) + try: + import win32evtlogutil, win32evtlog + self.appname = appname + self._welu = win32evtlogutil + if not dllname: + dllname = os.path.split(self._welu.__file__) + dllname = os.path.split(dllname[0]) + dllname = os.path.join(dllname[0], r'win32service.pyd') + self.dllname = dllname + self.logtype = logtype + self._welu.AddSourceToRegistry(appname, dllname, logtype) + self.deftype = win32evtlog.EVENTLOG_ERROR_TYPE + self.typemap = { + logging.DEBUG : win32evtlog.EVENTLOG_INFORMATION_TYPE, + logging.INFO : win32evtlog.EVENTLOG_INFORMATION_TYPE, + logging.WARNING : win32evtlog.EVENTLOG_WARNING_TYPE, + logging.ERROR : win32evtlog.EVENTLOG_ERROR_TYPE, + logging.CRITICAL: win32evtlog.EVENTLOG_ERROR_TYPE, + } + except ImportError: + print("The Python Win32 extensions for NT (service, event "\ + "logging) appear not to be available.") + self._welu = None + + def getMessageID(self, record): + """ + Return the message ID for the event record. If you are using your + own messages, you could do this by having the msg passed to the + logger being an ID rather than a formatting string. Then, in here, + you could use a dictionary lookup to get the message ID. This + version returns 1, which is the base message ID in win32service.pyd. + """ + return 1 + + def getEventCategory(self, record): + """ + Return the event category for the record. + + Override this if you want to specify your own categories. This version + returns 0. + """ + return 0 + + def getEventType(self, record): + """ + Return the event type for the record. + + Override this if you want to specify your own types. This version does + a mapping using the handler's typemap attribute, which is set up in + __init__() to a dictionary which contains mappings for DEBUG, INFO, + WARNING, ERROR and CRITICAL. If you are using your own levels you will + either need to override this method or place a suitable dictionary in + the handler's typemap attribute. + """ + return self.typemap.get(record.levelno, self.deftype) + + def emit(self, record): + """ + Emit a record. + + Determine the message ID, event category and event type. Then + log the message in the NT event log. + """ + if self._welu: + try: + id = self.getMessageID(record) + cat = self.getEventCategory(record) + type = self.getEventType(record) + msg = self.format(record) + self._welu.ReportEvent(self.appname, id, cat, type, [msg]) + except Exception: + self.handleError(record) + + def close(self): + """ + Clean up this handler. + + You can remove the application name from the registry as a + source of event log entries. However, if you do this, you will + not be able to see the events as you intended in the Event Log + Viewer - it needs to be able to access the registry to get the + DLL name. + """ + #self._welu.RemoveSourceFromRegistry(self.appname, self.logtype) + logging.Handler.close(self) + +class HTTPHandler(logging.Handler): + """ + A class which sends records to a Web server, using either GET or + POST semantics. + """ + def __init__(self, host, url, method="GET", secure=False, credentials=None, + context=None): + """ + Initialize the instance with the host, the request URL, and the method + ("GET" or "POST") + """ + logging.Handler.__init__(self) + method = method.upper() + if method not in ["GET", "POST"]: + raise ValueError("method must be GET or POST") + if not secure and context is not None: + raise ValueError("context parameter only makes sense " + "with secure=True") + self.host = host + self.url = url + self.method = method + self.secure = secure + self.credentials = credentials + self.context = context + + def mapLogRecord(self, record): + """ + Default implementation of mapping the log record into a dict + that is sent as the CGI data. Overwrite in your class. + Contributed by Franz Glasner. + """ + return record.__dict__ + + def emit(self, record): + """ + Emit a record. + + Send the record to the Web server as a percent-encoded dictionary + """ + try: + import http.client, urllib.parse + host = self.host + if self.secure: + h = http.client.HTTPSConnection(host, context=self.context) + else: + h = http.client.HTTPConnection(host) + url = self.url + data = urllib.parse.urlencode(self.mapLogRecord(record)) + if self.method == "GET": + if (url.find('?') >= 0): + sep = '&' + else: + sep = '?' + url = url + "%c%s" % (sep, data) + h.putrequest(self.method, url) + # support multiple hosts on one IP address... + # need to strip optional :port from host, if present + i = host.find(":") + if i >= 0: + host = host[:i] + # See issue #30904: putrequest call above already adds this header + # on Python 3.x. + # h.putheader("Host", host) + if self.method == "POST": + h.putheader("Content-type", + "application/x-www-form-urlencoded") + h.putheader("Content-length", str(len(data))) + if self.credentials: + import base64 + s = ('%s:%s' % self.credentials).encode('utf-8') + s = 'Basic ' + base64.b64encode(s).strip().decode('ascii') + h.putheader('Authorization', s) + h.endheaders() + if self.method == "POST": + h.send(data.encode('utf-8')) + h.getresponse() #can't do anything with the result + except Exception: + self.handleError(record) + +class BufferingHandler(logging.Handler): + """ + A handler class which buffers logging records in memory. Whenever each + record is added to the buffer, a check is made to see if the buffer should + be flushed. If it should, then flush() is expected to do what's needed. + """ + def __init__(self, capacity): + """ + Initialize the handler with the buffer size. + """ + logging.Handler.__init__(self) + self.capacity = capacity + self.buffer = [] + + def shouldFlush(self, record): + """ + Should the handler flush its buffer? + + Returns true if the buffer is up to capacity. This method can be + overridden to implement custom flushing strategies. + """ + return (len(self.buffer) >= self.capacity) + + def emit(self, record): + """ + Emit a record. + + Append the record. If shouldFlush() tells us to, call flush() to process + the buffer. + """ + self.buffer.append(record) + if self.shouldFlush(record): + self.flush() + + def flush(self): + """ + Override to implement custom flushing behaviour. + + This version just zaps the buffer to empty. + """ + self.acquire() + try: + self.buffer = [] + finally: + self.release() + + def close(self): + """ + Close the handler. + + This version just flushes and chains to the parent class' close(). + """ + try: + self.flush() + finally: + logging.Handler.close(self) + +class MemoryHandler(BufferingHandler): + """ + A handler class which buffers logging records in memory, periodically + flushing them to a target handler. Flushing occurs whenever the buffer + is full, or when an event of a certain severity or greater is seen. + """ + def __init__(self, capacity, flushLevel=logging.ERROR, target=None, + flushOnClose=True): + """ + Initialize the handler with the buffer size, the level at which + flushing should occur and an optional target. + + Note that without a target being set either here or via setTarget(), + a MemoryHandler is no use to anyone! + + The ``flushOnClose`` argument is ``True`` for backward compatibility + reasons - the old behaviour is that when the handler is closed, the + buffer is flushed, even if the flush level hasn't been exceeded nor the + capacity exceeded. To prevent this, set ``flushOnClose`` to ``False``. + """ + BufferingHandler.__init__(self, capacity) + self.flushLevel = flushLevel + self.target = target + # See Issue #26559 for why this has been added + self.flushOnClose = flushOnClose + + def shouldFlush(self, record): + """ + Check for buffer full or a record at the flushLevel or higher. + """ + return (len(self.buffer) >= self.capacity) or \ + (record.levelno >= self.flushLevel) + + def setTarget(self, target): + """ + Set the target handler for this handler. + """ + self.target = target + + def flush(self): + """ + For a MemoryHandler, flushing means just sending the buffered + records to the target, if there is one. Override if you want + different behaviour. + + The record buffer is also cleared by this operation. + """ + self.acquire() + try: + if self.target: + for record in self.buffer: + self.target.handle(record) + self.buffer = [] + finally: + self.release() + + def close(self): + """ + Flush, if appropriately configured, set the target to None and lose the + buffer. + """ + try: + if self.flushOnClose: + self.flush() + finally: + self.acquire() + try: + self.target = None + BufferingHandler.close(self) + finally: + self.release() + + +class QueueHandler(logging.Handler): + """ + This handler sends events to a queue. Typically, it would be used together + with a multiprocessing Queue to centralise logging to file in one process + (in a multi-process application), so as to avoid file write contention + between processes. + + This code is new in Python 3.2, but this class can be copy pasted into + user code for use with earlier Python versions. + """ + + def __init__(self, queue): + """ + Initialise an instance, using the passed queue. + """ + logging.Handler.__init__(self) + self.queue = queue + + def enqueue(self, record): + """ + Enqueue a record. + + The base implementation uses put_nowait. You may want to override + this method if you want to use blocking, timeouts or custom queue + implementations. + """ + self.queue.put_nowait(record) + + def prepare(self, record): + """ + Prepares a record for queuing. The object returned by this method is + enqueued. + + The base implementation formats the record to merge the message + and arguments, and removes unpickleable items from the record + in-place. + + You might want to override this method if you want to convert + the record to a dict or JSON string, or send a modified copy + of the record while leaving the original intact. + """ + # The format operation gets traceback text into record.exc_text + # (if there's exception data), and also puts the message into + # record.message. We can then use this to replace the original + # msg + args, as these might be unpickleable. We also zap the + # exc_info attribute, as it's no longer needed and, if not None, + # will typically not be pickleable. + self.format(record) + record.msg = record.message + record.args = None + record.exc_info = None + return record + + def emit(self, record): + """ + Emit a record. + + Writes the LogRecord to the queue, preparing it for pickling first. + """ + try: + self.enqueue(self.prepare(record)) + except Exception: + self.handleError(record) + +if threading: + class QueueListener(object): + """ + This class implements an internal threaded listener which watches for + LogRecords being added to a queue, removes them and passes them to a + list of handlers for processing. + """ + _sentinel = None + + def __init__(self, queue, *handlers, respect_handler_level=False): + """ + Initialise an instance with the specified queue and + handlers. + """ + self.queue = queue + self.handlers = handlers + self._thread = None + self.respect_handler_level = respect_handler_level + + def dequeue(self, block): + """ + Dequeue a record and return it, optionally blocking. + + The base implementation uses get. You may want to override this method + if you want to use timeouts or work with custom queue implementations. + """ + return self.queue.get(block) + + def start(self): + """ + Start the listener. + + This starts up a background thread to monitor the queue for + LogRecords to process. + """ + self._thread = t = threading.Thread(target=self._monitor) + t.daemon = True + t.start() + + def prepare(self , record): + """ + Prepare a record for handling. + + This method just returns the passed-in record. You may want to + override this method if you need to do any custom marshalling or + manipulation of the record before passing it to the handlers. + """ + return record + + def handle(self, record): + """ + Handle a record. + + This just loops through the handlers offering them the record + to handle. + """ + record = self.prepare(record) + for handler in self.handlers: + if not self.respect_handler_level: + process = True + else: + process = record.levelno >= handler.level + if process: + handler.handle(record) + + def _monitor(self): + """ + Monitor the queue for records, and ask the handler + to deal with them. + + This method runs on a separate, internal thread. + The thread will terminate if it sees a sentinel object in the queue. + """ + q = self.queue + has_task_done = hasattr(q, 'task_done') + while True: + try: + record = self.dequeue(True) + if record is self._sentinel: + break + self.handle(record) + if has_task_done: + q.task_done() + except queue.Empty: + break + + def enqueue_sentinel(self): + """ + This is used to enqueue the sentinel record. + + The base implementation uses put_nowait. You may want to override this + method if you want to use timeouts or work with custom queue + implementations. + """ + self.queue.put_nowait(self._sentinel) + + def stop(self): + """ + Stop the listener. + + This asks the thread to terminate, and then waits for it to do so. + Note that if you don't call this before your application exits, there + may be some records still left on the queue, which won't be processed. + """ + self.enqueue_sentinel() + self._thread.join() + self._thread = None \ No newline at end of file diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/LICENSE b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/LICENSE new file mode 100644 index 00000000..6f62d44e --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/LICENSE.APACHE b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/LICENSE.APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/LICENSE.APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/LICENSE.BSD b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/LICENSE.BSD new file mode 100644 index 00000000..42ce7b75 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/LICENSE.BSD @@ -0,0 +1,23 @@ +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/__about__.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/__about__.py new file mode 100644 index 00000000..26947283 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/__about__.py @@ -0,0 +1,27 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +from __future__ import absolute_import, division, print_function + +__all__ = [ + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", +] + +__title__ = "packaging" +__summary__ = "Core utilities for Python packages" +__uri__ = "https://github.com/pypa/packaging" + +__version__ = "20.0" + +__author__ = "Donald Stufft and individual contributors" +__email__ = "donald@stufft.io" + +__license__ = "BSD or Apache License, Version 2.0" +__copyright__ = "Copyright 2014-2019 %s" % __author__ diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/__init__.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/__init__.py new file mode 100644 index 00000000..a0cf67df --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/__init__.py @@ -0,0 +1,26 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +from __future__ import absolute_import, division, print_function + +from .__about__ import ( + __author__, + __copyright__, + __email__, + __license__, + __summary__, + __title__, + __uri__, + __version__, +) + +__all__ = [ + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", +] diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/_compat.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/_compat.py new file mode 100644 index 00000000..954d3999 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/_compat.py @@ -0,0 +1,42 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + +from __future__ import absolute_import, division, print_function + +import sys + +from ._typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: # pragma: no cover + from ..typing import Any, Dict, Tuple, Type + + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +# flake8: noqa + +if PY3: + string_types = (str,) +else: + string_types = (basestring,) + + +def with_metaclass(meta, *bases): + # type: (Type[Any], Tuple[Type[Any], ...]) -> Any + """ + Create a base class with a metaclass. + """ + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): # type: ignore + def __new__(cls, name, this_bases, d): + # type: (Type[Any], str, Tuple[Any], Dict[Any, Any]) -> Any + return meta(name, bases, d) + + return type.__new__(metaclass, "temporary_class", (), {}) \ No newline at end of file diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/_structures.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/_structures.py new file mode 100644 index 00000000..800d5c55 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/_structures.py @@ -0,0 +1,86 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +from __future__ import absolute_import, division, print_function + + +class InfinityType(object): + def __repr__(self): + # type: () -> str + return "Infinity" + + def __hash__(self): + # type: () -> int + return hash(repr(self)) + + def __lt__(self, other): + # type: (object) -> bool + return False + + def __le__(self, other): + # type: (object) -> bool + return False + + def __eq__(self, other): + # type: (object) -> bool + return isinstance(other, self.__class__) + + def __ne__(self, other): + # type: (object) -> bool + return not isinstance(other, self.__class__) + + def __gt__(self, other): + # type: (object) -> bool + return True + + def __ge__(self, other): + # type: (object) -> bool + return True + + def __neg__(self): + # type: (object) -> NegativeInfinityType + return NegativeInfinity + + +Infinity = InfinityType() + + +class NegativeInfinityType(object): + def __repr__(self): + # type: () -> str + return "-Infinity" + + def __hash__(self): + # type: () -> int + return hash(repr(self)) + + def __lt__(self, other): + # type: (object) -> bool + return True + + def __le__(self, other): + # type: (object) -> bool + return True + + def __eq__(self, other): + # type: (object) -> bool + return isinstance(other, self.__class__) + + def __ne__(self, other): + # type: (object) -> bool + return not isinstance(other, self.__class__) + + def __gt__(self, other): + # type: (object) -> bool + return False + + def __ge__(self, other): + # type: (object) -> bool + return False + + def __neg__(self): + # type: (object) -> InfinityType + return Infinity + + +NegativeInfinity = NegativeInfinityType() diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/_typing.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/_typing.py new file mode 100644 index 00000000..ce60f0e2 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/_typing.py @@ -0,0 +1,43 @@ +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + +"""For neatly implementing static typing in packaging. + +`mypy` - the static type analysis tool we use - uses the `typing` module, which +provides core functionality fundamental to mypy's functioning. + +Generally, `typing` would be imported at runtime and used in that fashion - +it acts as a no-op at runtime and does not have any run-time overhead by +design. + +As it turns out, `typing` is not vendorable - it uses separate sources for +Python 2/Python 3. Thus, this codebase can not expect it to be present. +To work around this, mypy allows the typing import to be behind a False-y +optional to prevent it from running at runtime and type-comments can be used +to remove the need for the types to be accessible directly during runtime. + +This module provides the False-y guard in a nicely named fashion so that a +curious maintainer can reach here to read this. + +In packaging, all static-typing related imports should be guarded as follows: + + from packaging._typing import MYPY_CHECK_RUNNING + + if MYPY_CHECK_RUNNING: + from typing import ... + +Ref: https://github.com/python/mypy/issues/3216 +""" + +MYPY_CHECK_RUNNING = False + +if MYPY_CHECK_RUNNING: # pragma: no cover + from .. import typing + + cast = typing.cast +else: + # typing's cast() is needed at runtime, but we don't want to import typing. + # Thus, we use a dummy no-op version, which we tell mypy to ignore. + def cast(type_, value): # type: ignore + return value \ No newline at end of file diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/markers.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/markers.py new file mode 100644 index 00000000..c455437b --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/markers.py @@ -0,0 +1,332 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + +from __future__ import absolute_import, division, print_function + +import operator +import os +import platform +import sys + +from pyparsing import ParseException, ParseResults, stringStart, stringEnd +from pyparsing import ZeroOrMore, Group, Forward, QuotedString +from pyparsing import Literal as L # noqa + +from ._compat import string_types +from ._typing import MYPY_CHECK_RUNNING +from .specifiers import Specifier, InvalidSpecifier + +if MYPY_CHECK_RUNNING: # pragma: no cover + from ..typing import Any, Callable, Dict, List, Optional, Tuple, Union + + Operator = Callable[[str, str], bool] + + +__all__ = [ + "InvalidMarker", + "UndefinedComparison", + "UndefinedEnvironmentName", + "Marker", + "default_environment", +] + + +class InvalidMarker(ValueError): + """ + An invalid marker was found, users should refer to PEP 508. + """ + + +class UndefinedComparison(ValueError): + """ + An invalid operation was attempted on a value that doesn't support it. + """ + + +class UndefinedEnvironmentName(ValueError): + """ + A name was attempted to be used that does not exist inside of the + environment. + """ + + +class Node(object): + def __init__(self, value): + # type: (Any) -> None + self.value = value + + def __str__(self): + # type: () -> str + return str(self.value) + + def __repr__(self): + # type: () -> str + return "<{0}({1!r})>".format(self.__class__.__name__, str(self)) + + def serialize(self): + # type: () -> str + raise NotImplementedError + + +class Variable(Node): + def serialize(self): + # type: () -> str + return str(self) + + +class Value(Node): + def serialize(self): + # type: () -> str + return '"{0}"'.format(self) + + +class Op(Node): + def serialize(self): + # type: () -> str + return str(self) + + +VARIABLE = ( + L("implementation_version") + | L("platform_python_implementation") + | L("implementation_name") + | L("python_full_version") + | L("platform_release") + | L("platform_version") + | L("platform_machine") + | L("platform_system") + | L("python_version") + | L("sys_platform") + | L("os_name") + | L("os.name") # PEP-345 + | L("sys.platform") # PEP-345 + | L("platform.version") # PEP-345 + | L("platform.machine") # PEP-345 + | L("platform.python_implementation") # PEP-345 + | L("python_implementation") # undocumented setuptools legacy + | L("extra") # PEP-508 +) +ALIASES = { + "os.name": "os_name", + "sys.platform": "sys_platform", + "platform.version": "platform_version", + "platform.machine": "platform_machine", + "platform.python_implementation": "platform_python_implementation", + "python_implementation": "platform_python_implementation", +} +VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0]))) + +VERSION_CMP = ( + L("===") | L("==") | L(">=") | L("<=") | L("!=") | L("~=") | L(">") | L("<") +) + +MARKER_OP = VERSION_CMP | L("not in") | L("in") +MARKER_OP.setParseAction(lambda s, l, t: Op(t[0])) + +MARKER_VALUE = QuotedString("'") | QuotedString('"') +MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0])) + +BOOLOP = L("and") | L("or") + +MARKER_VAR = VARIABLE | MARKER_VALUE + +MARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR) +MARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0])) + +LPAREN = L("(").suppress() +RPAREN = L(")").suppress() + +MARKER_EXPR = Forward() +MARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN) +MARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR) + +MARKER = stringStart + MARKER_EXPR + stringEnd + + +def _coerce_parse_result(results): + # type: (Union[ParseResults, List[Any]]) -> List[Any] + if isinstance(results, ParseResults): + return [_coerce_parse_result(i) for i in results] + else: + return results + + +def _format_marker(marker, first=True): + # type: (Union[List[str], Tuple[Node, ...], str], Optional[bool]) -> str + + assert isinstance(marker, (list, tuple, string_types)) + + # Sometimes we have a structure like [[...]] which is a single item list + # where the single item is itself it's own list. In that case we want skip + # the rest of this function so that we don't get extraneous () on the + # outside. + if ( + isinstance(marker, list) + and len(marker) == 1 + and isinstance(marker[0], (list, tuple)) + ): + return _format_marker(marker[0]) + + if isinstance(marker, list): + inner = (_format_marker(m, first=False) for m in marker) + if first: + return " ".join(inner) + else: + return "(" + " ".join(inner) + ")" + elif isinstance(marker, tuple): + return " ".join([m.serialize() for m in marker]) + else: + return marker + + +_operators = { + "in": lambda lhs, rhs: lhs in rhs, + "not in": lambda lhs, rhs: lhs not in rhs, + "<": operator.lt, + "<=": operator.le, + "==": operator.eq, + "!=": operator.ne, + ">=": operator.ge, + ">": operator.gt, +} # type: Dict[str, Operator] + + +def _eval_op(lhs, op, rhs): + # type: (str, Op, str) -> bool + try: + spec = Specifier("".join([op.serialize(), rhs])) + except InvalidSpecifier: + pass + else: + return spec.contains(lhs) + + oper = _operators.get(op.serialize()) # type: Optional[Operator] + if oper is None: + raise UndefinedComparison( + "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs) + ) + + return oper(lhs, rhs) + + +class Undefined(object): + pass + + +_undefined = Undefined() + + +def _get_env(environment, name): + # type: (Dict[str, str], str) -> str + value = environment.get(name, _undefined) # type: Union[str, Undefined] + + if isinstance(value, Undefined): + raise UndefinedEnvironmentName( + "{0!r} does not exist in evaluation environment.".format(name) + ) + + return value + + +def _evaluate_markers(markers, environment): + # type: (List[Any], Dict[str, str]) -> bool + groups = [[]] # type: List[List[bool]] + + for marker in markers: + assert isinstance(marker, (list, tuple, string_types)) + + if isinstance(marker, list): + groups[-1].append(_evaluate_markers(marker, environment)) + elif isinstance(marker, tuple): + lhs, op, rhs = marker + + if isinstance(lhs, Variable): + lhs_value = _get_env(environment, lhs.value) + rhs_value = rhs.value + else: + lhs_value = lhs.value + rhs_value = _get_env(environment, rhs.value) + + groups[-1].append(_eval_op(lhs_value, op, rhs_value)) + else: + assert marker in ["and", "or"] + if marker == "or": + groups.append([]) + + return any(all(item) for item in groups) + + +def format_full_version(info): + # type: (sys._version_info) -> str + version = "{0.major}.{0.minor}.{0.micro}".format(info) + kind = info.releaselevel + if kind != "final": + version += kind[0] + str(info.serial) + return version + + +def default_environment(): + # type: () -> Dict[str, str] + if hasattr(sys, "implementation"): + # Ignoring the `sys.implementation` reference for type checking due to + # mypy not liking that the attribute doesn't exist in Python 2.7 when + # run with the `--py27` flag. + iver = format_full_version(sys.implementation.version) # type: ignore + implementation_name = sys.implementation.name # type: ignore + else: + iver = "0" + implementation_name = "" + + return { + "implementation_name": implementation_name, + "implementation_version": iver, + "os_name": os.name, + "platform_machine": platform.machine(), + "platform_release": platform.release(), + "platform_system": platform.system(), + "platform_version": platform.version(), + "python_full_version": platform.python_version(), + "platform_python_implementation": platform.python_implementation(), + "python_version": ".".join(platform.python_version_tuple()[:2]), + "sys_platform": sys.platform, + } + + +class Marker(object): + def __init__(self, marker): + # type: (str) -> None + try: + self._markers = _coerce_parse_result(MARKER.parseString(marker)) + except ParseException as e: + err_str = "Invalid marker: {0!r}, parse error at {1!r}".format( + marker, marker[e.loc : e.loc + 8] + ) + raise InvalidMarker(err_str) + + def __str__(self): + # type: () -> str + return _format_marker(self._markers) + + def __repr__(self): + # type: () -> str + return "".format(str(self)) + + def evaluate(self, environment=None): + # type: (Optional[Dict[str, str]]) -> bool + """Evaluate a marker. + + Return the boolean from evaluating the given marker against the + environment. environment is an optional argument to override all or + part of the determined environment. + + The environment is determined from the current Python process. + """ + current_environment = default_environment() + if environment is not None: + current_environment.update(environment) + + return _evaluate_markers(self._markers, current_environment) \ No newline at end of file diff --git a/image_occlusion_enhanced/imagesize/__init__.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/py.typed similarity index 100% rename from image_occlusion_enhanced/imagesize/__init__.py rename to src/image_occlusion_enhanced/libaddon/_vendor/packaging/py.typed diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/requirements.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/requirements.py new file mode 100644 index 00000000..e53361c6 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/requirements.py @@ -0,0 +1,149 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + +from __future__ import absolute_import, division, print_function + +import string +import re + +from pyparsing import stringStart, stringEnd, originalTextFor, ParseException +from pyparsing import ZeroOrMore, Word, Optional, Regex, Combine +from pyparsing import Literal as L # noqa +from six.moves.urllib import parse as urlparse + +from ._typing import MYPY_CHECK_RUNNING +from .markers import MARKER_EXPR, Marker +from .specifiers import LegacySpecifier, Specifier, SpecifierSet + +if MYPY_CHECK_RUNNING: # pragma: no cover + from ..typing import List + + +class InvalidRequirement(ValueError): + """ + An invalid requirement was found, users should refer to PEP 508. + """ + + +ALPHANUM = Word(string.ascii_letters + string.digits) + +LBRACKET = L("[").suppress() +RBRACKET = L("]").suppress() +LPAREN = L("(").suppress() +RPAREN = L(")").suppress() +COMMA = L(",").suppress() +SEMICOLON = L(";").suppress() +AT = L("@").suppress() + +PUNCTUATION = Word("-_.") +IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM) +IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END)) + +NAME = IDENTIFIER("name") +EXTRA = IDENTIFIER + +URI = Regex(r"[^ ]+")("url") +URL = AT + URI + +EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA) +EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras") + +VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE) +VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE) + +VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY +VERSION_MANY = Combine( + VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False +)("_raw_spec") +_VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)) +_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or "") + +VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier") +VERSION_SPEC.setParseAction(lambda s, l, t: t[1]) + +MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker") +MARKER_EXPR.setParseAction( + lambda s, l, t: Marker(s[t._original_start : t._original_end]) +) +MARKER_SEPARATOR = SEMICOLON +MARKER = MARKER_SEPARATOR + MARKER_EXPR + +VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER) +URL_AND_MARKER = URL + Optional(MARKER) + +NAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER) + +REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd +# pyparsing isn't thread safe during initialization, so we do it eagerly, see +# issue #104 +REQUIREMENT.parseString("x[]") + + +class Requirement(object): + """Parse a requirement. + + Parse a given requirement string into its parts, such as name, specifier, + URL, and extras. Raises InvalidRequirement on a badly-formed requirement + string. + """ + + # TODO: Can we test whether something is contained within a requirement? + # If so how do we do that? Do we need to test against the _name_ of + # the thing as well as the version? What about the markers? + # TODO: Can we normalize the name and extra name? + + def __init__(self, requirement_string): + # type: (str) -> None + try: + req = REQUIREMENT.parseString(requirement_string) + except ParseException as e: + raise InvalidRequirement( + 'Parse error at "{0!r}": {1}'.format( + requirement_string[e.loc : e.loc + 8], e.msg + ) + ) + + self.name = req.name + if req.url: + parsed_url = urlparse.urlparse(req.url) + if parsed_url.scheme == "file": + if urlparse.urlunparse(parsed_url) != req.url: + raise InvalidRequirement("Invalid URL given") + elif not (parsed_url.scheme and parsed_url.netloc) or ( + not parsed_url.scheme and not parsed_url.netloc + ): + raise InvalidRequirement("Invalid URL: {0}".format(req.url)) + self.url = req.url + else: + self.url = None + self.extras = set(req.extras.asList() if req.extras else []) + self.specifier = SpecifierSet(req.specifier) + self.marker = req.marker if req.marker else None + + def __str__(self): + # type: () -> str + parts = [self.name] # type: List[str] + + if self.extras: + parts.append("[{0}]".format(",".join(sorted(self.extras)))) + + if self.specifier: + parts.append(str(self.specifier)) + + if self.url: + parts.append("@ {0}".format(self.url)) + if self.marker: + parts.append(" ") + + if self.marker: + parts.append("; {0}".format(self.marker)) + + return "".join(parts) + + def __repr__(self): + # type: () -> str + return "".format(str(self)) \ No newline at end of file diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/specifiers.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/specifiers.py new file mode 100644 index 00000000..a73bf629 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/specifiers.py @@ -0,0 +1,853 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + +from __future__ import absolute_import, division, print_function + +import abc +import functools +import itertools +import re + +from ._compat import string_types, with_metaclass +from ._typing import MYPY_CHECK_RUNNING +from .version import Version, LegacyVersion, parse + +if MYPY_CHECK_RUNNING: # pragma: no cover + from ..typing import ( + List, + Dict, + Union, + Iterable, + Iterator, + Optional, + Callable, + Tuple, + FrozenSet, + ) + + ParsedVersion = Union[Version, LegacyVersion] + UnparsedVersion = Union[Version, LegacyVersion, str] + CallableOperator = Callable[[ParsedVersion, str], bool] + + +class InvalidSpecifier(ValueError): + """ + An invalid specifier was found, users should refer to PEP 440. + """ + + +class BaseSpecifier(with_metaclass(abc.ABCMeta, object)): # type: ignore + @abc.abstractmethod + def __str__(self): + # type: () -> str + """ + Returns the str representation of this Specifier like object. This + should be representative of the Specifier itself. + """ + + @abc.abstractmethod + def __hash__(self): + # type: () -> int + """ + Returns a hash value for this Specifier like object. + """ + + @abc.abstractmethod + def __eq__(self, other): + # type: (object) -> bool + """ + Returns a boolean representing whether or not the two Specifier like + objects are equal. + """ + + @abc.abstractmethod + def __ne__(self, other): + # type: (object) -> bool + """ + Returns a boolean representing whether or not the two Specifier like + objects are not equal. + """ + + @abc.abstractproperty + def prereleases(self): + # type: () -> Optional[bool] + """ + Returns whether or not pre-releases as a whole are allowed by this + specifier. + """ + + @prereleases.setter + def prereleases(self, value): + # type: (bool) -> None + """ + Sets whether or not pre-releases as a whole are allowed by this + specifier. + """ + + @abc.abstractmethod + def contains(self, item, prereleases=None): + # type: (str, Optional[bool]) -> bool + """ + Determines if the given item is contained within this specifier. + """ + + @abc.abstractmethod + def filter(self, iterable, prereleases=None): + # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion] + """ + Takes an iterable of items and filters them so that only items which + are contained within this specifier are allowed in it. + """ + + +class _IndividualSpecifier(BaseSpecifier): + + _operators = {} # type: Dict[str, str] + + def __init__(self, spec="", prereleases=None): + # type: (str, Optional[bool]) -> None + match = self._regex.search(spec) + if not match: + raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec)) + + self._spec = ( + match.group("operator").strip(), + match.group("version").strip(), + ) # type: Tuple[str, str] + + # Store whether or not this Specifier should accept prereleases + self._prereleases = prereleases + + def __repr__(self): + # type: () -> str + pre = ( + ", prereleases={0!r}".format(self.prereleases) + if self._prereleases is not None + else "" + ) + + return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre) + + def __str__(self): + # type: () -> str + return "{0}{1}".format(*self._spec) + + def __hash__(self): + # type: () -> int + return hash(self._spec) + + def __eq__(self, other): + # type: (object) -> bool + if isinstance(other, string_types): + try: + other = self.__class__(str(other)) + except InvalidSpecifier: + return NotImplemented + elif not isinstance(other, self.__class__): + return NotImplemented + + return self._spec == other._spec + + def __ne__(self, other): + # type: (object) -> bool + if isinstance(other, string_types): + try: + other = self.__class__(str(other)) + except InvalidSpecifier: + return NotImplemented + elif not isinstance(other, self.__class__): + return NotImplemented + + return self._spec != other._spec + + def _get_operator(self, op): + # type: (str) -> CallableOperator + operator_callable = getattr( + self, "_compare_{0}".format(self._operators[op]) + ) # type: CallableOperator + return operator_callable + + def _coerce_version(self, version): + # type: (UnparsedVersion) -> ParsedVersion + if not isinstance(version, (LegacyVersion, Version)): + version = parse(version) + return version + + @property + def operator(self): + # type: () -> str + return self._spec[0] + + @property + def version(self): + # type: () -> str + return self._spec[1] + + @property + def prereleases(self): + # type: () -> Optional[bool] + return self._prereleases + + @prereleases.setter + def prereleases(self, value): + # type: (bool) -> None + self._prereleases = value + + def __contains__(self, item): + # type: (str) -> bool + return self.contains(item) + + def contains(self, item, prereleases=None): + # type: (UnparsedVersion, Optional[bool]) -> bool + + # Determine if prereleases are to be allowed or not. + if prereleases is None: + prereleases = self.prereleases + + # Normalize item to a Version or LegacyVersion, this allows us to have + # a shortcut for ``"2.0" in Specifier(">=2") + normalized_item = self._coerce_version(item) + + # Determine if we should be supporting prereleases in this specifier + # or not, if we do not support prereleases than we can short circuit + # logic if this version is a prereleases. + if normalized_item.is_prerelease and not prereleases: + return False + + # Actually do the comparison to determine if this item is contained + # within this Specifier or not. + operator_callable = self._get_operator(self.operator) # type: CallableOperator + return operator_callable(normalized_item, self.version) + + def filter(self, iterable, prereleases=None): + # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion] + + yielded = False + found_prereleases = [] + + kw = {"prereleases": prereleases if prereleases is not None else True} + + # Attempt to iterate over all the values in the iterable and if any of + # them match, yield them. + for version in iterable: + parsed_version = self._coerce_version(version) + + if self.contains(parsed_version, **kw): + # If our version is a prerelease, and we were not set to allow + # prereleases, then we'll store it for later incase nothing + # else matches this specifier. + if parsed_version.is_prerelease and not ( + prereleases or self.prereleases + ): + found_prereleases.append(version) + # Either this is not a prerelease, or we should have been + # accepting prereleases from the beginning. + else: + yielded = True + yield version + + # Now that we've iterated over everything, determine if we've yielded + # any values, and if we have not and we have any prereleases stored up + # then we will go ahead and yield the prereleases. + if not yielded and found_prereleases: + for version in found_prereleases: + yield version + + +class LegacySpecifier(_IndividualSpecifier): + + _regex_str = r""" + (?P(==|!=|<=|>=|<|>)) + \s* + (?P + [^,;\s)]* # Since this is a "legacy" specifier, and the version + # string can be just about anything, we match everything + # except for whitespace, a semi-colon for marker support, + # a closing paren since versions can be enclosed in + # them, and a comma since it's a version separator. + ) + """ + + _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) + + _operators = { + "==": "equal", + "!=": "not_equal", + "<=": "less_than_equal", + ">=": "greater_than_equal", + "<": "less_than", + ">": "greater_than", + } + + def _coerce_version(self, version): + # type: (Union[ParsedVersion, str]) -> LegacyVersion + if not isinstance(version, LegacyVersion): + version = LegacyVersion(str(version)) + return version + + def _compare_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool + return prospective == self._coerce_version(spec) + + def _compare_not_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool + return prospective != self._coerce_version(spec) + + def _compare_less_than_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool + return prospective <= self._coerce_version(spec) + + def _compare_greater_than_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool + return prospective >= self._coerce_version(spec) + + def _compare_less_than(self, prospective, spec): + # type: (LegacyVersion, str) -> bool + return prospective < self._coerce_version(spec) + + def _compare_greater_than(self, prospective, spec): + # type: (LegacyVersion, str) -> bool + return prospective > self._coerce_version(spec) + + +def _require_version_compare( + fn # type: (Callable[[Specifier, ParsedVersion, str], bool]) +): + # type: (...) -> Callable[[Specifier, ParsedVersion, str], bool] + @functools.wraps(fn) + def wrapped(self, prospective, spec): + # type: (Specifier, ParsedVersion, str) -> bool + if not isinstance(prospective, Version): + return False + return fn(self, prospective, spec) + + return wrapped + + +class Specifier(_IndividualSpecifier): + + _regex_str = r""" + (?P(~=|==|!=|<=|>=|<|>|===)) + (?P + (?: + # The identity operators allow for an escape hatch that will + # do an exact string match of the version you wish to install. + # This will not be parsed by PEP 440 and we cannot determine + # any semantic meaning from it. This operator is discouraged + # but included entirely as an escape hatch. + (?<====) # Only match for the identity operator + \s* + [^\s]* # We just match everything, except for whitespace + # since we are only testing for strict identity. + ) + | + (?: + # The (non)equality operators allow for wild card and local + # versions to be specified so we have to define these two + # operators separately to enable that. + (?<===|!=) # Only match for equals and not equals + + \s* + v? + (?:[0-9]+!)? # epoch + [0-9]+(?:\.[0-9]+)* # release + (?: # pre release + [-_\.]? + (a|b|c|rc|alpha|beta|pre|preview) + [-_\.]? + [0-9]* + )? + (?: # post release + (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) + )? + + # You cannot use a wild card and a dev or local version + # together so group them with a | and make them optional. + (?: + (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release + (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local + | + \.\* # Wild card syntax of .* + )? + ) + | + (?: + # The compatible operator requires at least two digits in the + # release segment. + (?<=~=) # Only match for the compatible operator + + \s* + v? + (?:[0-9]+!)? # epoch + [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *) + (?: # pre release + [-_\.]? + (a|b|c|rc|alpha|beta|pre|preview) + [-_\.]? + [0-9]* + )? + (?: # post release + (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) + )? + (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release + ) + | + (?: + # All other operators only allow a sub set of what the + # (non)equality operators do. Specifically they do not allow + # local versions to be specified nor do they allow the prefix + # matching wild cards. + (?=": "greater_than_equal", + "<": "less_than", + ">": "greater_than", + "===": "arbitrary", + } + + @_require_version_compare + def _compare_compatible(self, prospective, spec): + # type: (ParsedVersion, str) -> bool + + # Compatible releases have an equivalent combination of >= and ==. That + # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to + # implement this in terms of the other specifiers instead of + # implementing it ourselves. The only thing we need to do is construct + # the other specifiers. + + # We want everything but the last item in the version, but we want to + # ignore post and dev releases and we want to treat the pre-release as + # it's own separate segment. + prefix = ".".join( + list( + itertools.takewhile( + lambda x: (not x.startswith("post") and not x.startswith("dev")), + _version_split(spec), + ) + )[:-1] + ) + + # Add the prefix notation to the end of our string + prefix += ".*" + + return self._get_operator(">=")(prospective, spec) and self._get_operator("==")( + prospective, prefix + ) + + @_require_version_compare + def _compare_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool + + # We need special logic to handle prefix matching + if spec.endswith(".*"): + # In the case of prefix matching we want to ignore local segment. + prospective = Version(prospective.public) + # Split the spec out by dots, and pretend that there is an implicit + # dot in between a release segment and a pre-release segment. + split_spec = _version_split(spec[:-2]) # Remove the trailing .* + + # Split the prospective version out by dots, and pretend that there + # is an implicit dot in between a release segment and a pre-release + # segment. + split_prospective = _version_split(str(prospective)) + + # Shorten the prospective version to be the same length as the spec + # so that we can determine if the specifier is a prefix of the + # prospective version or not. + shortened_prospective = split_prospective[: len(split_spec)] + + # Pad out our two sides with zeros so that they both equal the same + # length. + padded_spec, padded_prospective = _pad_version( + split_spec, shortened_prospective + ) + + return padded_prospective == padded_spec + else: + # Convert our spec string into a Version + spec_version = Version(spec) + + # If the specifier does not have a local segment, then we want to + # act as if the prospective version also does not have a local + # segment. + if not spec_version.local: + prospective = Version(prospective.public) + + return prospective == spec_version + + @_require_version_compare + def _compare_not_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool + return not self._compare_equal(prospective, spec) + + @_require_version_compare + def _compare_less_than_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool + return prospective <= Version(spec) + + @_require_version_compare + def _compare_greater_than_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool + return prospective >= Version(spec) + + @_require_version_compare + def _compare_less_than(self, prospective, spec_str): + # type: (ParsedVersion, str) -> bool + + # Convert our spec to a Version instance, since we'll want to work with + # it as a version. + spec = Version(spec_str) + + # Check to see if the prospective version is less than the spec + # version. If it's not we can short circuit and just return False now + # instead of doing extra unneeded work. + if not prospective < spec: + return False + + # This special case is here so that, unless the specifier itself + # includes is a pre-release version, that we do not accept pre-release + # versions for the version mentioned in the specifier (e.g. <3.1 should + # not match 3.1.dev0, but should match 3.0.dev0). + if not spec.is_prerelease and prospective.is_prerelease: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # If we've gotten to here, it means that prospective version is both + # less than the spec version *and* it's not a pre-release of the same + # version in the spec. + return True + + @_require_version_compare + def _compare_greater_than(self, prospective, spec_str): + # type: (ParsedVersion, str) -> bool + + # Convert our spec to a Version instance, since we'll want to work with + # it as a version. + spec = Version(spec_str) + + # Check to see if the prospective version is greater than the spec + # version. If it's not we can short circuit and just return False now + # instead of doing extra unneeded work. + if not prospective > spec: + return False + + # This special case is here so that, unless the specifier itself + # includes is a post-release version, that we do not accept + # post-release versions for the version mentioned in the specifier + # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). + if not spec.is_postrelease and prospective.is_postrelease: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # Ensure that we do not allow a local version of the version mentioned + # in the specifier, which is technically greater than, to match. + if prospective.local is not None: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # If we've gotten to here, it means that prospective version is both + # greater than the spec version *and* it's not a pre-release of the + # same version in the spec. + return True + + def _compare_arbitrary(self, prospective, spec): + # type: (Version, str) -> bool + return str(prospective).lower() == str(spec).lower() + + @property + def prereleases(self): + # type: () -> bool + + # If there is an explicit prereleases set for this, then we'll just + # blindly use that. + if self._prereleases is not None: + return self._prereleases + + # Look at all of our specifiers and determine if they are inclusive + # operators, and if they are if they are including an explicit + # prerelease. + operator, version = self._spec + if operator in ["==", ">=", "<=", "~=", "==="]: + # The == specifier can include a trailing .*, if it does we + # want to remove before parsing. + if operator == "==" and version.endswith(".*"): + version = version[:-2] + + # Parse the version, and if it is a pre-release than this + # specifier allows pre-releases. + if parse(version).is_prerelease: + return True + + return False + + @prereleases.setter + def prereleases(self, value): + # type: (bool) -> None + self._prereleases = value + + +_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") + + +def _version_split(version): + # type: (str) -> List[str] + result = [] # type: List[str] + for item in version.split("."): + match = _prefix_regex.search(item) + if match: + result.extend(match.groups()) + else: + result.append(item) + return result + + +def _pad_version(left, right): + # type: (List[str], List[str]) -> Tuple[List[str], List[str]] + left_split, right_split = [], [] + + # Get the release segment of our versions + left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left))) + right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) + + # Get the rest of our versions + left_split.append(left[len(left_split[0]) :]) + right_split.append(right[len(right_split[0]) :]) + + # Insert our padding + left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) + right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) + + return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split))) + + +class SpecifierSet(BaseSpecifier): + def __init__(self, specifiers="", prereleases=None): + # type: (str, Optional[bool]) -> None + + # Split on , to break each individual specifier into it's own item, and + # strip each item to remove leading/trailing whitespace. + split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] + + # Parsed each individual specifier, attempting first to make it a + # Specifier and falling back to a LegacySpecifier. + parsed = set() + for specifier in split_specifiers: + try: + parsed.add(Specifier(specifier)) + except InvalidSpecifier: + parsed.add(LegacySpecifier(specifier)) + + # Turn our parsed specifiers into a frozen set and save them for later. + self._specs = frozenset(parsed) + + # Store our prereleases value so we can use it later to determine if + # we accept prereleases or not. + self._prereleases = prereleases + + def __repr__(self): + # type: () -> str + pre = ( + ", prereleases={0!r}".format(self.prereleases) + if self._prereleases is not None + else "" + ) + + return "".format(str(self), pre) + + def __str__(self): + # type: () -> str + return ",".join(sorted(str(s) for s in self._specs)) + + def __hash__(self): + # type: () -> int + return hash(self._specs) + + def __and__(self, other): + # type: (Union[SpecifierSet, str]) -> SpecifierSet + if isinstance(other, string_types): + other = SpecifierSet(other) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + specifier = SpecifierSet() + specifier._specs = frozenset(self._specs | other._specs) + + if self._prereleases is None and other._prereleases is not None: + specifier._prereleases = other._prereleases + elif self._prereleases is not None and other._prereleases is None: + specifier._prereleases = self._prereleases + elif self._prereleases == other._prereleases: + specifier._prereleases = self._prereleases + else: + raise ValueError( + "Cannot combine SpecifierSets with True and False prerelease " + "overrides." + ) + + return specifier + + def __eq__(self, other): + # type: (object) -> bool + if isinstance(other, (string_types, _IndividualSpecifier)): + other = SpecifierSet(str(other)) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + return self._specs == other._specs + + def __ne__(self, other): + # type: (object) -> bool + if isinstance(other, (string_types, _IndividualSpecifier)): + other = SpecifierSet(str(other)) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + return self._specs != other._specs + + def __len__(self): + # type: () -> int + return len(self._specs) + + def __iter__(self): + # type: () -> Iterator[FrozenSet[_IndividualSpecifier]] + return iter(self._specs) + + @property + def prereleases(self): + # type: () -> Optional[bool] + + # If we have been given an explicit prerelease modifier, then we'll + # pass that through here. + if self._prereleases is not None: + return self._prereleases + + # If we don't have any specifiers, and we don't have a forced value, + # then we'll just return None since we don't know if this should have + # pre-releases or not. + if not self._specs: + return None + + # Otherwise we'll see if any of the given specifiers accept + # prereleases, if any of them do we'll return True, otherwise False. + return any(s.prereleases for s in self._specs) + + @prereleases.setter + def prereleases(self, value): + # type: (bool) -> None + self._prereleases = value + + def __contains__(self, item): + # type: (Union[ParsedVersion, str]) -> bool + return self.contains(item) + + def contains(self, item, prereleases=None): + # type: (Union[ParsedVersion, str], Optional[bool]) -> bool + + # Ensure that our item is a Version or LegacyVersion instance. + if not isinstance(item, (LegacyVersion, Version)): + item = parse(item) + + # Determine if we're forcing a prerelease or not, if we're not forcing + # one for this particular filter call, then we'll use whatever the + # SpecifierSet thinks for whether or not we should support prereleases. + if prereleases is None: + prereleases = self.prereleases + + # We can determine if we're going to allow pre-releases by looking to + # see if any of the underlying items supports them. If none of them do + # and this item is a pre-release then we do not allow it and we can + # short circuit that here. + # Note: This means that 1.0.dev1 would not be contained in something + # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0 + if not prereleases and item.is_prerelease: + return False + + # We simply dispatch to the underlying specs here to make sure that the + # given version is contained within all of them. + # Note: This use of all() here means that an empty set of specifiers + # will always return True, this is an explicit design decision. + return all(s.contains(item, prereleases=prereleases) for s in self._specs) + + def filter( + self, + iterable, # type: Iterable[Union[ParsedVersion, str]] + prereleases=None, # type: Optional[bool] + ): + # type: (...) -> Iterable[Union[ParsedVersion, str]] + + # Determine if we're forcing a prerelease or not, if we're not forcing + # one for this particular filter call, then we'll use whatever the + # SpecifierSet thinks for whether or not we should support prereleases. + if prereleases is None: + prereleases = self.prereleases + + # If we have any specifiers, then we want to wrap our iterable in the + # filter method for each one, this will act as a logical AND amongst + # each specifier. + if self._specs: + for spec in self._specs: + iterable = spec.filter(iterable, prereleases=bool(prereleases)) + return iterable + # If we do not have any specifiers, then we need to have a rough filter + # which will filter out any pre-releases, unless there are no final + # releases, and which will filter out LegacyVersion in general. + else: + filtered = [] # type: List[Union[ParsedVersion, str]] + found_prereleases = [] # type: List[Union[ParsedVersion, str]] + + for item in iterable: + # Ensure that we some kind of Version class for this item. + if not isinstance(item, (LegacyVersion, Version)): + parsed_version = parse(item) + else: + parsed_version = item + + # Filter out any item which is parsed as a LegacyVersion + if isinstance(parsed_version, LegacyVersion): + continue + + # Store any item which is a pre-release for later unless we've + # already found a final version or we are accepting prereleases + if parsed_version.is_prerelease and not prereleases: + if not filtered: + found_prereleases.append(item) + else: + filtered.append(item) + + # If we've found no items except for pre-releases, then we'll go + # ahead and use the pre-releases + if not filtered and found_prereleases and prereleases is None: + return found_prereleases + + return filtered \ No newline at end of file diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/tags.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/tags.py new file mode 100644 index 00000000..c336ea08 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/tags.py @@ -0,0 +1,735 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + + +from __future__ import absolute_import + +import distutils.util + +try: + from importlib.machinery import EXTENSION_SUFFIXES +except ImportError: # pragma: no cover + import imp + + EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] + del imp +from .. import logging +import os +import platform +import re +import struct +import sys +import sysconfig +import warnings + +from ._typing import MYPY_CHECK_RUNNING, cast + +if MYPY_CHECK_RUNNING: # pragma: no cover + from ..typing import ( + Dict, + FrozenSet, + IO, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, + ) + + PythonVersion = Sequence[int] + MacVersion = Tuple[int, int] + GlibcVersion = Tuple[int, int] + + +logger = logging.getLogger(__name__) + +INTERPRETER_SHORT_NAMES = { + "python": "py", # Generic. + "cpython": "cp", + "pypy": "pp", + "ironpython": "ip", + "jython": "jy", +} # type: Dict[str, str] + + +_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 + + +class Tag(object): + + __slots__ = ["_interpreter", "_abi", "_platform"] + + def __init__(self, interpreter, abi, platform): + # type: (str, str, str) -> None + self._interpreter = interpreter.lower() + self._abi = abi.lower() + self._platform = platform.lower() + + @property + def interpreter(self): + # type: () -> str + return self._interpreter + + @property + def abi(self): + # type: () -> str + return self._abi + + @property + def platform(self): + # type: () -> str + return self._platform + + def __eq__(self, other): + # type: (object) -> bool + if not isinstance(other, Tag): + return NotImplemented + + return ( + (self.platform == other.platform) + and (self.abi == other.abi) + and (self.interpreter == other.interpreter) + ) + + def __hash__(self): + # type: () -> int + return hash((self._interpreter, self._abi, self._platform)) + + def __str__(self): + # type: () -> str + return "{}-{}-{}".format(self._interpreter, self._abi, self._platform) + + def __repr__(self): + # type: () -> str + return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) + + +def parse_tag(tag): + # type: (str) -> FrozenSet[Tag] + tags = set() + interpreters, abis, platforms = tag.split("-") + for interpreter in interpreters.split("."): + for abi in abis.split("."): + for platform_ in platforms.split("."): + tags.add(Tag(interpreter, abi, platform_)) + return frozenset(tags) + + +def _warn_keyword_parameter(func_name, kwargs): + # type: (str, Dict[str, bool]) -> bool + """ + Backwards-compatibility with Python 2.7 to allow treating 'warn' as keyword-only. + """ + if not kwargs: + return False + elif len(kwargs) > 1 or "warn" not in kwargs: + kwargs.pop("warn", None) + arg = next(iter(kwargs.keys())) + raise TypeError( + "{}() got an unexpected keyword argument {!r}".format(func_name, arg) + ) + return kwargs["warn"] + + +def _get_config_var(name, warn=False): + # type: (str, bool) -> Union[int, str, None] + value = sysconfig.get_config_var(name) + if value is None and warn: + logger.debug( + "Config variable '%s' is unset, Python ABI tag may be incorrect", name + ) + return value + + +def _normalize_string(string): + # type: (str) -> str + return string.replace(".", "_").replace("-", "_") + + +def _abi3_applies(python_version): + # type: (PythonVersion) -> bool + """ + Determine if the Python version supports abi3. + + PEP 384 was first implemented in Python 3.2. + """ + return len(python_version) > 1 and tuple(python_version) >= (3, 2) + + +def _cpython_abis(py_version, warn=False): + # type: (PythonVersion, bool) -> List[str] + py_version = tuple(py_version) # To allow for version comparison. + abis = [] + version = "{}{}".format(*py_version[:2]) + debug = pymalloc = ucs4 = "" + with_debug = _get_config_var("Py_DEBUG", warn) + has_refcount = hasattr(sys, "gettotalrefcount") + # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled + # extension modules is the best option. + # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 + has_ext = "_d.pyd" in EXTENSION_SUFFIXES + if with_debug or (with_debug is None and (has_refcount or has_ext)): + debug = "d" + if py_version < (3, 8): + with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) + if with_pymalloc or with_pymalloc is None: + pymalloc = "m" + if py_version < (3, 3): + unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) + if unicode_size == 4 or ( + unicode_size is None and sys.maxunicode == 0x10FFFF + ): + ucs4 = "u" + elif debug: + # Debug builds can also load "normal" extension modules. + # We can also assume no UCS-4 or pymalloc requirement. + abis.append("cp{version}".format(version=version)) + abis.insert( + 0, + "cp{version}{debug}{pymalloc}{ucs4}".format( + version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 + ), + ) + return abis + + +def cpython_tags( + python_version=None, # type: Optional[PythonVersion] + abis=None, # type: Optional[Iterable[str]] + platforms=None, # type: Optional[Iterable[str]] + **kwargs # type: bool +): + # type: (...) -> Iterator[Tag] + """ + Yields the tags for a CPython interpreter. + + The tags consist of: + - cp-- + - cp-abi3- + - cp-none- + - cp-abi3- # Older Python versions down to 3.2. + + If python_version only specifies a major version then user-provided ABIs and + the 'none' ABItag will be used. + + If 'abi3' or 'none' are specified in 'abis' then they will be yielded at + their normal position and not at the beginning. + """ + warn = _warn_keyword_parameter("cpython_tags", kwargs) + if not python_version: + python_version = sys.version_info[:2] + + if len(python_version) < 2: + interpreter = "cp{}".format(python_version[0]) + else: + interpreter = "cp{}{}".format(*python_version[:2]) + + if abis is None: + if len(python_version) > 1: + abis = _cpython_abis(python_version, warn) + else: + abis = [] + abis = list(abis) + # 'abi3' and 'none' are explicitly handled later. + for explicit_abi in ("abi3", "none"): + try: + abis.remove(explicit_abi) + except ValueError: + pass + + platforms = list(platforms or _platform_tags()) + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + if _abi3_applies(python_version): + for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): + yield tag + for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms): + yield tag + + if _abi3_applies(python_version): + for minor_version in range(python_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{major}{minor}".format( + major=python_version[0], minor=minor_version + ) + yield Tag(interpreter, "abi3", platform_) + + +def _generic_abi(): + # type: () -> Iterator[str] + abi = sysconfig.get_config_var("SOABI") + if abi: + yield _normalize_string(abi) + + +def generic_tags( + interpreter=None, # type: Optional[str] + abis=None, # type: Optional[Iterable[str]] + platforms=None, # type: Optional[Iterable[str]] + **kwargs # type: bool +): + # type: (...) -> Iterator[Tag] + """ + Yields the tags for a generic interpreter. + + The tags consist of: + - -- + + The "none" ABI will be added if it was not explicitly provided. + """ + warn = _warn_keyword_parameter("generic_tags", kwargs) + if not interpreter: + interp_name = interpreter_name() + interp_version = interpreter_version(warn=warn) + interpreter = "".join([interp_name, interp_version]) + if abis is None: + abis = _generic_abi() + platforms = list(platforms or _platform_tags()) + abis = list(abis) + if "none" not in abis: + abis.append("none") + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + + +def _py_interpreter_range(py_version): + # type: (PythonVersion) -> Iterator[str] + """ + Yields Python versions in descending order. + + After the latest version, the major-only version will be yielded, and then + all previous versions of that major version. + """ + if len(py_version) > 1: + yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) + yield "py{major}".format(major=py_version[0]) + if len(py_version) > 1: + for minor in range(py_version[1] - 1, -1, -1): + yield "py{major}{minor}".format(major=py_version[0], minor=minor) + + +def compatible_tags( + python_version=None, # type: Optional[PythonVersion] + interpreter=None, # type: Optional[str] + platforms=None, # type: Optional[Iterator[str]] +): + # type: (...) -> Iterator[Tag] + """ + Yields the sequence of tags that are compatible with a specific version of Python. + + The tags consist of: + - py*-none- + - -none-any # ... if `interpreter` is provided. + - py*-none-any + """ + if not python_version: + python_version = sys.version_info[:2] + if not platforms: + platforms = _platform_tags() + for version in _py_interpreter_range(python_version): + for platform_ in platforms: + yield Tag(version, "none", platform_) + if interpreter: + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(python_version): + yield Tag(version, "none", "any") + + +def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): + # type: (str, bool) -> str + if not is_32bit: + return arch + + if arch.startswith("ppc"): + return "ppc" + + return "i386" + + +def _mac_binary_formats(version, cpu_arch): + # type: (MacVersion, str) -> List[str] + formats = [cpu_arch] + if cpu_arch == "x86_64": + if version < (10, 4): + return [] + formats.extend(["intel", "fat64", "fat32"]) + + elif cpu_arch == "i386": + if version < (10, 4): + return [] + formats.extend(["intel", "fat32", "fat"]) + + elif cpu_arch == "ppc64": + # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? + if version > (10, 5) or version < (10, 4): + return [] + formats.append("fat64") + + elif cpu_arch == "ppc": + if version > (10, 6): + return [] + formats.extend(["fat32", "fat"]) + + formats.append("universal") + return formats + + +def mac_platforms(version=None, arch=None): + # type: (Optional[MacVersion], Optional[str]) -> Iterator[str] + """ + Yields the platform tags for a macOS system. + + The `version` parameter is a two-item tuple specifying the macOS version to + generate platform tags for. The `arch` parameter is the CPU architecture to + generate platform tags for. Both parameters default to the appropriate value + for the current system. + """ + version_str, _, cpu_arch = platform.mac_ver() # type: ignore + if version is None: + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + else: + version = version + if arch is None: + arch = _mac_arch(cpu_arch) + else: + arch = arch + for minor_version in range(version[1], -1, -1): + compat_version = version[0], minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + + +# From PEP 513. +def _is_manylinux_compatible(name, glibc_version): + # type: (str, GlibcVersion) -> bool + # Check for presence of _manylinux module. + try: + import _manylinux # noqa + + return bool(getattr(_manylinux, name + "_compatible")) + except (ImportError, AttributeError): + # Fall through to heuristic check below. + pass + + return _have_compatible_glibc(*glibc_version) + + +def _glibc_version_string(): + # type: () -> Optional[str] + # Returns glibc version string, or None if not using glibc. + return _glibc_version_string_confstr() or _glibc_version_string_ctypes() + + +def _glibc_version_string_confstr(): + # type: () -> Optional[str] + """ + Primary implementation of glibc_version_string using os.confstr. + """ + # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely + # to be broken or missing. This strategy is used in the standard library + # platform module. + # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183 + try: + # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17". + version_string = os.confstr( # type: ignore[attr-defined] # noqa: F821 + "CS_GNU_LIBC_VERSION" + ) + assert version_string is not None + _, version = version_string.split() # type: Tuple[str, str] + except (AssertionError, AttributeError, OSError, ValueError): + # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... + return None + return version + + +def _glibc_version_string_ctypes(): + # type: () -> Optional[str] + """ + Fallback implementation of glibc_version_string using ctypes. + """ + try: + import ctypes + except ImportError: + return None + + # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen + # manpage says, "If filename is NULL, then the returned handle is for the + # main program". This way we can let the linker do the work to figure out + # which libc our process is actually using. + # + # Note: typeshed is wrong here so we are ignoring this line. + process_namespace = ctypes.CDLL(None) # type: ignore + try: + gnu_get_libc_version = process_namespace.gnu_get_libc_version + except AttributeError: + # Symbol doesn't exist -> therefore, we are not linked to + # glibc. + return None + + # Call gnu_get_libc_version, which returns a string like "2.5" + gnu_get_libc_version.restype = ctypes.c_char_p + version_str = gnu_get_libc_version() # type: str + # py2 / py3 compatibility: + if not isinstance(version_str, str): + version_str = version_str.decode("ascii") + + return version_str + + +# Separated out from have_compatible_glibc for easier unit testing. +def _check_glibc_version(version_str, required_major, minimum_minor): + # type: (str, int, int) -> bool + # Parse string and check against requested version. + # + # We use a regexp instead of str.split because we want to discard any + # random junk that might come after the minor version -- this might happen + # in patched/forked versions of glibc (e.g. Linaro's version of glibc + # uses version strings like "2.20-2014.11"). See gh-3588. + m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) + if not m: + warnings.warn( + "Expected glibc version with 2 components major.minor," + " got: %s" % version_str, + RuntimeWarning, + ) + return False + return ( + int(m.group("major")) == required_major + and int(m.group("minor")) >= minimum_minor + ) + + +def _have_compatible_glibc(required_major, minimum_minor): + # type: (int, int) -> bool + version_str = _glibc_version_string() + if version_str is None: + return False + return _check_glibc_version(version_str, required_major, minimum_minor) + + +# Python does not provide platform information at sufficient granularity to +# identify the architecture of the running executable in some cases, so we +# determine it dynamically by reading the information from the running +# process. This only applies on Linux, which uses the ELF format. +class _ELFFileHeader(object): + # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + class _InvalidELFFileHeader(ValueError): + """ + An invalid ELF file header was found. + """ + + ELF_MAGIC_NUMBER = 0x7F454C46 + ELFCLASS32 = 1 + ELFCLASS64 = 2 + ELFDATA2LSB = 1 + ELFDATA2MSB = 2 + EM_386 = 3 + EM_S390 = 22 + EM_ARM = 40 + EM_X86_64 = 62 + EF_ARM_ABIMASK = 0xFF000000 + EF_ARM_ABI_VER5 = 0x05000000 + EF_ARM_ABI_FLOAT_HARD = 0x00000400 + + def __init__(self, file): + # type: (IO[bytes]) -> None + def unpack(fmt): + # type: (str) -> int + try: + result, = struct.unpack( + fmt, file.read(struct.calcsize(fmt)) + ) # type: (int, ) + except struct.error: + raise _ELFFileHeader._InvalidELFFileHeader() + return result + + self.e_ident_magic = unpack(">I") + if self.e_ident_magic != self.ELF_MAGIC_NUMBER: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_class = unpack("B") + if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_data = unpack("B") + if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_version = unpack("B") + self.e_ident_osabi = unpack("B") + self.e_ident_abiversion = unpack("B") + self.e_ident_pad = file.read(7) + format_h = "H" + format_i = "I" + format_q = "Q" + format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q + self.e_type = unpack(format_h) + self.e_machine = unpack(format_h) + self.e_version = unpack(format_i) + self.e_entry = unpack(format_p) + self.e_phoff = unpack(format_p) + self.e_shoff = unpack(format_p) + self.e_flags = unpack(format_i) + self.e_ehsize = unpack(format_h) + self.e_phentsize = unpack(format_h) + self.e_phnum = unpack(format_h) + self.e_shentsize = unpack(format_h) + self.e_shnum = unpack(format_h) + self.e_shstrndx = unpack(format_h) + + +def _get_elf_header(): + # type: () -> Optional[_ELFFileHeader] + try: + with open(sys.executable, "rb") as f: + elf_header = _ELFFileHeader(f) + except (IOError, OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader): + return None + return elf_header + + +def _is_linux_armhf(): + # type: () -> bool + # hard-float ABI can be detected from the ELF header of the running + # process + # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf + elf_header = _get_elf_header() + if elf_header is None: + return False + result = elf_header.e_ident_class == elf_header.ELFCLASS32 + result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB + result &= elf_header.e_machine == elf_header.EM_ARM + result &= ( + elf_header.e_flags & elf_header.EF_ARM_ABIMASK + ) == elf_header.EF_ARM_ABI_VER5 + result &= ( + elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD + ) == elf_header.EF_ARM_ABI_FLOAT_HARD + return result + + +def _is_linux_i686(): + # type: () -> bool + elf_header = _get_elf_header() + if elf_header is None: + return False + result = elf_header.e_ident_class == elf_header.ELFCLASS32 + result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB + result &= elf_header.e_machine == elf_header.EM_386 + return result + + +def _have_compatible_manylinux_abi(arch): + # type: (str) -> bool + if arch == "armv7l": + return _is_linux_armhf() + if arch == "i686": + return _is_linux_i686() + return True + + +def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): + # type: (bool) -> Iterator[str] + linux = _normalize_string(distutils.util.get_platform()) + if linux == "linux_x86_64" and is_32bit: + linux = "linux_i686" + manylinux_support = [] + _, arch = linux.split("_", 1) + if _have_compatible_manylinux_abi(arch): + if arch in {"x86_64", "i686", "aarch64", "armv7l", "ppc64", "ppc64le", "s390x"}: + manylinux_support.append( + ("manylinux2014", (2, 17)) + ) # CentOS 7 w/ glibc 2.17 (PEP 599) + if arch in {"x86_64", "i686"}: + manylinux_support.append( + ("manylinux2010", (2, 12)) + ) # CentOS 6 w/ glibc 2.12 (PEP 571) + manylinux_support.append( + ("manylinux1", (2, 5)) + ) # CentOS 5 w/ glibc 2.5 (PEP 513) + manylinux_support_iter = iter(manylinux_support) + for name, glibc_version in manylinux_support_iter: + if _is_manylinux_compatible(name, glibc_version): + yield linux.replace("linux", name) + break + # Support for a later manylinux implies support for an earlier version. + for name, _ in manylinux_support_iter: + yield linux.replace("linux", name) + yield linux + + +def _generic_platforms(): + # type: () -> Iterator[str] + yield _normalize_string(distutils.util.get_platform()) + + +def _platform_tags(): + # type: () -> Iterator[str] + """ + Provides the platform tags for this installation. + """ + if platform.system() == "Darwin": + return mac_platforms() + elif platform.system() == "Linux": + return _linux_platforms() + else: + return _generic_platforms() + + +def interpreter_name(): + # type: () -> str + """ + Returns the name of the running interpreter. + """ + try: + name = sys.implementation.name # type: ignore + except AttributeError: # pragma: no cover + # Python 2.7 compatibility. + name = platform.python_implementation().lower() + return INTERPRETER_SHORT_NAMES.get(name) or name + + +def interpreter_version(**kwargs): + # type: (bool) -> str + """ + Returns the version of the running interpreter. + """ + warn = _warn_keyword_parameter("interpreter_version", kwargs) + version = _get_config_var("py_version_nodot", warn=warn) + if version: + version = str(version) + else: + version = "".join(map(str, sys.version_info[:2])) + return version + + +def sys_tags(**kwargs): + # type: (bool) -> Iterator[Tag] + """ + Returns the sequence of tag triples for the running interpreter. + + The order of the sequence corresponds to priority order for the + interpreter, from most to least important. + """ + warn = _warn_keyword_parameter("sys_tags", kwargs) + + interp_name = interpreter_name() + if interp_name == "cp": + for tag in cpython_tags(warn=warn): + yield tag + else: + for tag in generic_tags(): + yield tag + + for tag in compatible_tags(): + yield tag \ No newline at end of file diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/utils.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/utils.py new file mode 100644 index 00000000..74b885d8 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/utils.py @@ -0,0 +1,66 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + +from __future__ import absolute_import, division, print_function + +import re + +from ._typing import MYPY_CHECK_RUNNING +from .version import InvalidVersion, Version + +if MYPY_CHECK_RUNNING: # pragma: no cover + from ..typing import Union + +_canonicalize_regex = re.compile(r"[-_.]+") + + +def canonicalize_name(name): + # type: (str) -> str + # This is taken from PEP 503. + return _canonicalize_regex.sub("-", name).lower() + + +def canonicalize_version(_version): + # type: (str) -> Union[Version, str] + """ + This is very similar to Version.__str__, but has one subtle difference + with the way it handles the release segment. + """ + + try: + version = Version(_version) + except InvalidVersion: + # Legacy versions cannot be normalized + return _version + + parts = [] + + # Epoch + if version.epoch != 0: + parts.append("{0}!".format(version.epoch)) + + # Release segment + # NB: This strips trailing '.0's to normalize + parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in version.release))) + + # Pre-release + if version.pre is not None: + parts.append("".join(str(x) for x in version.pre)) + + # Post-release + if version.post is not None: + parts.append(".post{0}".format(version.post)) + + # Development release + if version.dev is not None: + parts.append(".dev{0}".format(version.dev)) + + # Local version segment + if version.local is not None: + parts.append("+{0}".format(version.local)) + + return "".join(parts) \ No newline at end of file diff --git a/src/image_occlusion_enhanced/libaddon/_vendor/packaging/version.py b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/version.py new file mode 100644 index 00000000..6c35f24d --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/_vendor/packaging/version.py @@ -0,0 +1,539 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +# NOTE: This module has been modified to be packaged with Anki add-ons +# The changes are Copyright (c) 2020 Aristotelis P. +# and licensed under the same license as the original module + +from __future__ import absolute_import, division, print_function + +import collections +import itertools +import re + +from ._structures import Infinity, NegativeInfinity +from ._typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: # pragma: no cover + from ..typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union + + from ._structures import InfinityType, NegativeInfinityType + + InfiniteTypes = Union[InfinityType, NegativeInfinityType] + PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] + SubLocalType = Union[InfiniteTypes, int, str] + LocalType = Union[ + NegativeInfinityType, + Tuple[ + Union[ + SubLocalType, + Tuple[SubLocalType, str], + Tuple[NegativeInfinityType, SubLocalType], + ], + ..., + ], + ] + CmpKey = Tuple[ + int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType + ] + LegacyCmpKey = Tuple[int, Tuple[str, ...]] + VersionComparisonMethod = Callable[ + [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool + ] + +__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] + + +_Version = collections.namedtuple( + "_Version", ["epoch", "release", "dev", "pre", "post", "local"] +) + + +def parse(version): + # type: (str) -> Union[LegacyVersion, Version] + """ + Parse the given version string and return either a :class:`Version` object + or a :class:`LegacyVersion` object depending on if the given version is + a valid PEP 440 version or a legacy version. + """ + try: + return Version(version) + except InvalidVersion: + return LegacyVersion(version) + + +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + +class _BaseVersion(object): + _key = None # type: Union[CmpKey, LegacyCmpKey] + + def __hash__(self): + # type: () -> int + return hash(self._key) + + def __lt__(self, other): + # type: (_BaseVersion) -> bool + return self._compare(other, lambda s, o: s < o) + + def __le__(self, other): + # type: (_BaseVersion) -> bool + return self._compare(other, lambda s, o: s <= o) + + def __eq__(self, other): + # type: (object) -> bool + return self._compare(other, lambda s, o: s == o) + + def __ge__(self, other): + # type: (_BaseVersion) -> bool + return self._compare(other, lambda s, o: s >= o) + + def __gt__(self, other): + # type: (_BaseVersion) -> bool + return self._compare(other, lambda s, o: s > o) + + def __ne__(self, other): + # type: (object) -> bool + return self._compare(other, lambda s, o: s != o) + + def _compare(self, other, method): + # type: (object, VersionComparisonMethod) -> Union[bool, NotImplemented] + if not isinstance(other, _BaseVersion): + return NotImplemented + + return method(self._key, other._key) + + +class LegacyVersion(_BaseVersion): + def __init__(self, version): + # type: (str) -> None + self._version = str(version) + self._key = _legacy_cmpkey(self._version) + + def __str__(self): + # type: () -> str + return self._version + + def __repr__(self): + # type: () -> str + return "".format(repr(str(self))) + + @property + def public(self): + # type: () -> str + return self._version + + @property + def base_version(self): + # type: () -> str + return self._version + + @property + def epoch(self): + # type: () -> int + return -1 + + @property + def release(self): + # type: () -> None + return None + + @property + def pre(self): + # type: () -> None + return None + + @property + def post(self): + # type: () -> None + return None + + @property + def dev(self): + # type: () -> None + return None + + @property + def local(self): + # type: () -> None + return None + + @property + def is_prerelease(self): + # type: () -> bool + return False + + @property + def is_postrelease(self): + # type: () -> bool + return False + + @property + def is_devrelease(self): + # type: () -> bool + return False + + +_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) + +_legacy_version_replacement_map = { + "pre": "c", + "preview": "c", + "-": "final-", + "rc": "c", + "dev": "@", +} + + +def _parse_version_parts(s): + # type: (str) -> Iterator[str] + for part in _legacy_version_component_re.split(s): + part = _legacy_version_replacement_map.get(part, part) + + if not part or part == ".": + continue + + if part[:1] in "0123456789": + # pad for numeric comparison + yield part.zfill(8) + else: + yield "*" + part + + # ensure that alpha/beta/candidate are before final + yield "*final" + + +def _legacy_cmpkey(version): + # type: (str) -> LegacyCmpKey + + # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch + # greater than or equal to 0. This will effectively put the LegacyVersion, + # which uses the defacto standard originally implemented by setuptools, + # as before all PEP 440 versions. + epoch = -1 + + # This scheme is taken from pkg_resources.parse_version setuptools prior to + # it's adoption of the packaging library. + parts = [] # type: List[str] + for part in _parse_version_parts(version.lower()): + if part.startswith("*"): + # remove "-" before a prerelease tag + if part < "*final": + while parts and parts[-1] == "*final-": + parts.pop() + + # remove trailing zeros from each series of numeric parts + while parts and parts[-1] == "00000000": + parts.pop() + + parts.append(part) + + return epoch, tuple(parts) + + +# Deliberately not anchored to the start and end of the string, to make it +# easier for 3rd party code to reuse +VERSION_PATTERN = r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                                # pre-release
      +            [-_\.]?
      +            (?P(a|b|c|rc|alpha|beta|pre|preview))
      +            [-_\.]?
      +            (?P[0-9]+)?
      +        )?
      +        (?P                                         # post release
      +            (?:-(?P[0-9]+))
      +            |
      +            (?:
      +                [-_\.]?
      +                (?Ppost|rev|r)
      +                [-_\.]?
      +                (?P[0-9]+)?
      +            )
      +        )?
      +        (?P                                          # dev release
      +            [-_\.]?
      +            (?Pdev)
      +            [-_\.]?
      +            (?P[0-9]+)?
      +        )?
      +    )
      +    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
      +"""
      +
      +
      +class Version(_BaseVersion):
      +
      +    _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
      +
      +    def __init__(self, version):
      +        # type: (str) -> None
      +
      +        # Validate the version and parse it into pieces
      +        match = self._regex.search(version)
      +        if not match:
      +            raise InvalidVersion("Invalid version: '{0}'".format(version))
      +
      +        # Store the parsed out pieces of the version
      +        self._version = _Version(
      +            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
      +            release=tuple(int(i) for i in match.group("release").split(".")),
      +            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
      +            post=_parse_letter_version(
      +                match.group("post_l"), match.group("post_n1") or match.group("post_n2")
      +            ),
      +            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
      +            local=_parse_local_version(match.group("local")),
      +        )
      +
      +        # Generate a key which will be used for sorting
      +        self._key = _cmpkey(
      +            self._version.epoch,
      +            self._version.release,
      +            self._version.pre,
      +            self._version.post,
      +            self._version.dev,
      +            self._version.local,
      +        )
      +
      +    def __repr__(self):
      +        # type: () -> str
      +        return "".format(repr(str(self)))
      +
      +    def __str__(self):
      +        # type: () -> str
      +        parts = []
      +
      +        # Epoch
      +        if self.epoch != 0:
      +            parts.append("{0}!".format(self.epoch))
      +
      +        # Release segment
      +        parts.append(".".join(str(x) for x in self.release))
      +
      +        # Pre-release
      +        if self.pre is not None:
      +            parts.append("".join(str(x) for x in self.pre))
      +
      +        # Post-release
      +        if self.post is not None:
      +            parts.append(".post{0}".format(self.post))
      +
      +        # Development release
      +        if self.dev is not None:
      +            parts.append(".dev{0}".format(self.dev))
      +
      +        # Local version segment
      +        if self.local is not None:
      +            parts.append("+{0}".format(self.local))
      +
      +        return "".join(parts)
      +
      +    @property
      +    def epoch(self):
      +        # type: () -> int
      +        _epoch = self._version.epoch  # type: int
      +        return _epoch
      +
      +    @property
      +    def release(self):
      +        # type: () -> Tuple[int, ...]
      +        _release = self._version.release  # type: Tuple[int, ...]
      +        return _release
      +
      +    @property
      +    def pre(self):
      +        # type: () -> Optional[Tuple[str, int]]
      +        _pre = self._version.pre  # type: Optional[Tuple[str, int]]
      +        return _pre
      +
      +    @property
      +    def post(self):
      +        # type: () -> Optional[Tuple[str, int]]
      +        return self._version.post[1] if self._version.post else None
      +
      +    @property
      +    def dev(self):
      +        # type: () -> Optional[Tuple[str, int]]
      +        return self._version.dev[1] if self._version.dev else None
      +
      +    @property
      +    def local(self):
      +        # type: () -> Optional[str]
      +        if self._version.local:
      +            return ".".join(str(x) for x in self._version.local)
      +        else:
      +            return None
      +
      +    @property
      +    def public(self):
      +        # type: () -> str
      +        return str(self).split("+", 1)[0]
      +
      +    @property
      +    def base_version(self):
      +        # type: () -> str
      +        parts = []
      +
      +        # Epoch
      +        if self.epoch != 0:
      +            parts.append("{0}!".format(self.epoch))
      +
      +        # Release segment
      +        parts.append(".".join(str(x) for x in self.release))
      +
      +        return "".join(parts)
      +
      +    @property
      +    def is_prerelease(self):
      +        # type: () -> bool
      +        return self.dev is not None or self.pre is not None
      +
      +    @property
      +    def is_postrelease(self):
      +        # type: () -> bool
      +        return self.post is not None
      +
      +    @property
      +    def is_devrelease(self):
      +        # type: () -> bool
      +        return self.dev is not None
      +
      +    @property
      +    def major(self):
      +        # type: () -> int
      +        return self.release[0] if len(self.release) >= 1 else 0
      +
      +    @property
      +    def minor(self):
      +        # type: () -> int
      +        return self.release[1] if len(self.release) >= 2 else 0
      +
      +    @property
      +    def micro(self):
      +        # type: () -> int
      +        return self.release[2] if len(self.release) >= 3 else 0
      +
      +
      +def _parse_letter_version(
      +    letter,  # type: str
      +    number,  # type: Union[str, bytes, SupportsInt]
      +):
      +    # type: (...) -> Optional[Tuple[str, int]]
      +
      +    if letter:
      +        # We consider there to be an implicit 0 in a pre-release if there is
      +        # not a numeral associated with it.
      +        if number is None:
      +            number = 0
      +
      +        # We normalize any letters to their lower case form
      +        letter = letter.lower()
      +
      +        # We consider some words to be alternate spellings of other words and
      +        # in those cases we want to normalize the spellings to our preferred
      +        # spelling.
      +        if letter == "alpha":
      +            letter = "a"
      +        elif letter == "beta":
      +            letter = "b"
      +        elif letter in ["c", "pre", "preview"]:
      +            letter = "rc"
      +        elif letter in ["rev", "r"]:
      +            letter = "post"
      +
      +        return letter, int(number)
      +    if not letter and number:
      +        # We assume if we are given a number, but we are not given a letter
      +        # then this is using the implicit post release syntax (e.g. 1.0-1)
      +        letter = "post"
      +
      +        return letter, int(number)
      +
      +    return None
      +
      +
      +_local_version_separators = re.compile(r"[\._-]")
      +
      +
      +def _parse_local_version(local):
      +    # type: (str) -> Optional[LocalType]
      +    """
      +    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
      +    """
      +    if local is not None:
      +        return tuple(
      +            part.lower() if not part.isdigit() else int(part)
      +            for part in _local_version_separators.split(local)
      +        )
      +    return None
      +
      +
      +def _cmpkey(
      +    epoch,  # type: int
      +    release,  # type: Tuple[int, ...]
      +    pre,  # type: Optional[Tuple[str, int]]
      +    post,  # type: Optional[Tuple[str, int]]
      +    dev,  # type: Optional[Tuple[str, int]]
      +    local,  # type: Optional[Tuple[SubLocalType]]
      +):
      +    # type: (...) -> CmpKey
      +
      +    # When we compare a release version, we want to compare it with all of the
      +    # trailing zeros removed. So we'll use a reverse the list, drop all the now
      +    # leading zeros until we come to something non zero, then take the rest
      +    # re-reverse it back into the correct order and make it a tuple and use
      +    # that for our sorting key.
      +    _release = tuple(
      +        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
      +    )
      +
      +    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
      +    # We'll do this by abusing the pre segment, but we _only_ want to do this
      +    # if there is not a pre or a post segment. If we have one of those then
      +    # the normal sorting rules will handle this case correctly.
      +    if pre is None and post is None and dev is not None:
      +        _pre = NegativeInfinity  # type: PrePostDevType
      +    # Versions without a pre-release (except as noted above) should sort after
      +    # those with one.
      +    elif pre is None:
      +        _pre = Infinity
      +    else:
      +        _pre = pre
      +
      +    # Versions without a post segment should sort before those with one.
      +    if post is None:
      +        _post = NegativeInfinity  # type: PrePostDevType
      +
      +    else:
      +        _post = post
      +
      +    # Versions without a development segment should sort after those with one.
      +    if dev is None:
      +        _dev = Infinity  # type: PrePostDevType
      +
      +    else:
      +        _dev = dev
      +
      +    if local is None:
      +        # Versions without a local segment should sort before those with one.
      +        _local = NegativeInfinity  # type: LocalType
      +    else:
      +        # Versions with a local segment need that segment parsed to implement
      +        # the sorting rules in PEP440.
      +        # - Alpha numeric segments sort before numeric segments
      +        # - Alpha numeric segments sort lexicographically
      +        # - Numeric segments sort numerically
      +        # - Shorter versions sort before longer versions when the prefixes
      +        #   match exactly
      +        _local = tuple(
      +            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
      +        )
      +
      +    return epoch, _release, _pre, _post, _dev, _local
      \ No newline at end of file
      diff --git a/src/image_occlusion_enhanced/libaddon/_vendor_legacy/PYTHON.LICENSE b/src/image_occlusion_enhanced/libaddon/_vendor_legacy/PYTHON.LICENSE
      new file mode 100644
      index 00000000..5c201804
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/_vendor_legacy/PYTHON.LICENSE
      @@ -0,0 +1,260 @@
      +This license pertains to the following Python standard library packages:
      +
      +types typing logging
      +
      +----------------------------- LICENSE TEXT -------------------------------
      +
      +A. HISTORY OF THE SOFTWARE
      +==========================
      +
      +Python was created in the early 1990s by Guido van Rossum at Stichting
      +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
      +as a successor of a language called ABC.  Guido remains Python's
      +principal author, although it includes many contributions from others.
      +
      +In 1995, Guido continued his work on Python at the Corporation for
      +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
      +in Reston, Virginia where he released several versions of the
      +software.
      +
      +In May 2000, Guido and the Python core development team moved to
      +BeOpen.com to form the BeOpen PythonLabs team.  In October of the same
      +year, the PythonLabs team moved to Digital Creations, which became
      +Zope Corporation.  In 2001, the Python Software Foundation (PSF, see
      +https://www.python.org/psf/) was formed, a non-profit organization
      +created specifically to own Python-related Intellectual Property.
      +Zope Corporation was a sponsoring member of the PSF.
      +
      +All Python releases are Open Source (see http://www.opensource.org for
      +the Open Source Definition).  Historically, most, but not all, Python
      +releases have also been GPL-compatible; the table below summarizes
      +the various releases.
      +
      +    Release         Derived     Year        Owner       GPL-
      +                    from                                compatible? (1)
      +
      +    0.9.0 thru 1.2              1991-1995   CWI         yes
      +    1.3 thru 1.5.2  1.2         1995-1999   CNRI        yes
      +    1.6             1.5.2       2000        CNRI        no
      +    2.0             1.6         2000        BeOpen.com  no
      +    1.6.1           1.6         2001        CNRI        yes (2)
      +    2.1             2.0+1.6.1   2001        PSF         no
      +    2.0.1           2.0+1.6.1   2001        PSF         yes
      +    2.1.1           2.1+2.0.1   2001        PSF         yes
      +    2.1.2           2.1.1       2002        PSF         yes
      +    2.1.3           2.1.2       2002        PSF         yes
      +    2.2 and above   2.1.1       2001-now    PSF         yes
      +
      +Footnotes:
      +
      +(1) GPL-compatible doesn't mean that we're distributing Python under
      +    the GPL.  All Python licenses, unlike the GPL, let you distribute
      +    a modified version without making your changes open source.  The
      +    GPL-compatible licenses make it possible to combine Python with
      +    other software that is released under the GPL; the others don't.
      +
      +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
      +    because its license has a choice of law clause.  According to
      +    CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
      +    is "not incompatible" with the GPL.
      +
      +Thanks to the many outside volunteers who have worked under Guido's
      +direction to make these releases possible.
      +
      +
      +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
      +===============================================================
      +
      +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
      +--------------------------------------------
      +
      +1. This LICENSE AGREEMENT is between the Python Software Foundation
      +("PSF"), and the Individual or Organization ("Licensee") accessing and
      +otherwise using this software ("Python") in source or binary form and
      +its associated documentation.
      +
      +2. Subject to the terms and conditions of this License Agreement, PSF hereby
      +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
      +analyze, test, perform and/or display publicly, prepare derivative works,
      +distribute, and otherwise use Python alone or in any derivative version,
      +provided, however, that PSF's License Agreement and PSF's notice of copyright,
      +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
      +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation; All
      +Rights Reserved" are retained in Python alone or in any derivative version
      +prepared by Licensee.
      +
      +3. In the event Licensee prepares a derivative work that is based on
      +or incorporates Python or any part thereof, and wants to make
      +the derivative work available to others as provided herein, then
      +Licensee hereby agrees to include in any such work a brief summary of
      +the changes made to Python.
      +
      +4. PSF is making Python available to Licensee on an "AS IS"
      +basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
      +IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
      +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
      +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
      +INFRINGE ANY THIRD PARTY RIGHTS.
      +
      +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
      +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
      +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
      +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
      +
      +6. This License Agreement will automatically terminate upon a material
      +breach of its terms and conditions.
      +
      +7. Nothing in this License Agreement shall be deemed to create any
      +relationship of agency, partnership, or joint venture between PSF and
      +Licensee.  This License Agreement does not grant permission to use PSF
      +trademarks or trade name in a trademark sense to endorse or promote
      +products or services of Licensee, or any third party.
      +
      +8. By copying, installing or otherwise using Python, Licensee
      +agrees to be bound by the terms and conditions of this License
      +Agreement.
      +
      +
      +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
      +-------------------------------------------
      +
      +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
      +
      +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
      +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
      +Individual or Organization ("Licensee") accessing and otherwise using
      +this software in source or binary form and its associated
      +documentation ("the Software").
      +
      +2. Subject to the terms and conditions of this BeOpen Python License
      +Agreement, BeOpen hereby grants Licensee a non-exclusive,
      +royalty-free, world-wide license to reproduce, analyze, test, perform
      +and/or display publicly, prepare derivative works, distribute, and
      +otherwise use the Software alone or in any derivative version,
      +provided, however, that the BeOpen Python License is retained in the
      +Software, alone or in any derivative version prepared by Licensee.
      +
      +3. BeOpen is making the Software available to Licensee on an "AS IS"
      +basis.  BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
      +IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
      +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
      +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
      +INFRINGE ANY THIRD PARTY RIGHTS.
      +
      +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
      +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
      +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
      +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
      +
      +5. This License Agreement will automatically terminate upon a material
      +breach of its terms and conditions.
      +
      +6. This License Agreement shall be governed by and interpreted in all
      +respects by the law of the State of California, excluding conflict of
      +law provisions.  Nothing in this License Agreement shall be deemed to
      +create any relationship of agency, partnership, or joint venture
      +between BeOpen and Licensee.  This License Agreement does not grant
      +permission to use BeOpen trademarks or trade names in a trademark
      +sense to endorse or promote products or services of Licensee, or any
      +third party.  As an exception, the "BeOpen Python" logos available at
      +http://www.pythonlabs.com/logos.html may be used according to the
      +permissions granted on that web page.
      +
      +7. By copying, installing or otherwise using the software, Licensee
      +agrees to be bound by the terms and conditions of this License
      +Agreement.
      +
      +
      +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
      +---------------------------------------
      +
      +1. This LICENSE AGREEMENT is between the Corporation for National
      +Research Initiatives, having an office at 1895 Preston White Drive,
      +Reston, VA 20191 ("CNRI"), and the Individual or Organization
      +("Licensee") accessing and otherwise using Python 1.6.1 software in
      +source or binary form and its associated documentation.
      +
      +2. Subject to the terms and conditions of this License Agreement, CNRI
      +hereby grants Licensee a nonexclusive, royalty-free, world-wide
      +license to reproduce, analyze, test, perform and/or display publicly,
      +prepare derivative works, distribute, and otherwise use Python 1.6.1
      +alone or in any derivative version, provided, however, that CNRI's
      +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
      +1995-2001 Corporation for National Research Initiatives; All Rights
      +Reserved" are retained in Python 1.6.1 alone or in any derivative
      +version prepared by Licensee.  Alternately, in lieu of CNRI's License
      +Agreement, Licensee may substitute the following text (omitting the
      +quotes): "Python 1.6.1 is made available subject to the terms and
      +conditions in CNRI's License Agreement.  This Agreement together with
      +Python 1.6.1 may be located on the Internet using the following
      +unique, persistent identifier (known as a handle): 1895.22/1013.  This
      +Agreement may also be obtained from a proxy server on the Internet
      +using the following URL: http://hdl.handle.net/1895.22/1013".
      +
      +3. In the event Licensee prepares a derivative work that is based on
      +or incorporates Python 1.6.1 or any part thereof, and wants to make
      +the derivative work available to others as provided herein, then
      +Licensee hereby agrees to include in any such work a brief summary of
      +the changes made to Python 1.6.1.
      +
      +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
      +basis.  CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
      +IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
      +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
      +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
      +INFRINGE ANY THIRD PARTY RIGHTS.
      +
      +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
      +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
      +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
      +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
      +
      +6. This License Agreement will automatically terminate upon a material
      +breach of its terms and conditions.
      +
      +7. This License Agreement shall be governed by the federal
      +intellectual property law of the United States, including without
      +limitation the federal copyright law, and, to the extent such
      +U.S. federal law does not apply, by the law of the Commonwealth of
      +Virginia, excluding Virginia's conflict of law provisions.
      +Notwithstanding the foregoing, with regard to derivative works based
      +on Python 1.6.1 that incorporate non-separable material that was
      +previously distributed under the GNU General Public License (GPL), the
      +law of the Commonwealth of Virginia shall govern this License
      +Agreement only as to issues arising under or with respect to
      +Paragraphs 4, 5, and 7 of this License Agreement.  Nothing in this
      +License Agreement shall be deemed to create any relationship of
      +agency, partnership, or joint venture between CNRI and Licensee.  This
      +License Agreement does not grant permission to use CNRI trademarks or
      +trade name in a trademark sense to endorse or promote products or
      +services of Licensee, or any third party.
      +
      +8. By clicking on the "ACCEPT" button where indicated, or by copying,
      +installing or otherwise using Python 1.6.1, Licensee agrees to be
      +bound by the terms and conditions of this License Agreement.
      +
      +        ACCEPT
      +
      +
      +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
      +--------------------------------------------------
      +
      +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
      +The Netherlands.  All rights reserved.
      +
      +Permission to use, copy, modify, and distribute this software and its
      +documentation for any purpose and without fee is hereby granted,
      +provided that the above copyright notice appear in all copies and that
      +both that copyright notice and this permission notice appear in
      +supporting documentation, and that the name of Stichting Mathematisch
      +Centrum or CWI not be used in advertising or publicity pertaining to
      +distribution of the software without specific, written prior
      +permission.
      +
      +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
      +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
      +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
      +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
      +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
      +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
      +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
      diff --git a/src/image_occlusion_enhanced/libaddon/_vendor_legacy/types.py b/src/image_occlusion_enhanced/libaddon/_vendor_legacy/types.py
      new file mode 100644
      index 00000000..d8d84709
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/_vendor_legacy/types.py
      @@ -0,0 +1,266 @@
      +"""
      +Define names for built-in types that aren't directly accessible as a builtin.
      +"""
      +import sys
      +
      +# Iterators in Python aren't a matter of type but of protocol.  A large
      +# and changing number of builtin types implement *some* flavor of
      +# iterator.  Don't check the type!  Use hasattr to check for both
      +# "__iter__" and "__next__" attributes instead.
      +
      +def _f(): pass
      +FunctionType = type(_f)
      +LambdaType = type(lambda: None)         # Same as FunctionType
      +CodeType = type(_f.__code__)
      +MappingProxyType = type(type.__dict__)
      +SimpleNamespace = type(sys.implementation)
      +
      +def _g():
      +    yield 1
      +GeneratorType = type(_g())
      +
      +async def _c(): pass
      +_c = _c()
      +CoroutineType = type(_c)
      +_c.close()  # Prevent ResourceWarning
      +
      +async def _ag():
      +    yield
      +_ag = _ag()
      +AsyncGeneratorType = type(_ag)
      +
      +class _C:
      +    def _m(self): pass
      +MethodType = type(_C()._m)
      +
      +BuiltinFunctionType = type(len)
      +BuiltinMethodType = type([].append)     # Same as BuiltinFunctionType
      +
      +ModuleType = type(sys)
      +
      +try:
      +    raise TypeError
      +except TypeError:
      +    tb = sys.exc_info()[2]
      +    TracebackType = type(tb)
      +    FrameType = type(tb.tb_frame)
      +    tb = None; del tb
      +
      +# For Jython, the following two types are identical
      +GetSetDescriptorType = type(FunctionType.__code__)
      +MemberDescriptorType = type(FunctionType.__globals__)
      +
      +del sys, _f, _g, _C, _c,                           # Not for export
      +
      +
      +# Provide a PEP 3115 compliant mechanism for class creation
      +def new_class(name, bases=(), kwds=None, exec_body=None):
      +    """Create a class object dynamically using the appropriate metaclass."""
      +    meta, ns, kwds = prepare_class(name, bases, kwds)
      +    if exec_body is not None:
      +        exec_body(ns)
      +    return meta(name, bases, ns, **kwds)
      +
      +def prepare_class(name, bases=(), kwds=None):
      +    """Call the __prepare__ method of the appropriate metaclass.
      +
      +    Returns (metaclass, namespace, kwds) as a 3-tuple
      +
      +    *metaclass* is the appropriate metaclass
      +    *namespace* is the prepared class namespace
      +    *kwds* is an updated copy of the passed in kwds argument with any
      +    'metaclass' entry removed. If no kwds argument is passed in, this will
      +    be an empty dict.
      +    """
      +    if kwds is None:
      +        kwds = {}
      +    else:
      +        kwds = dict(kwds) # Don't alter the provided mapping
      +    if 'metaclass' in kwds:
      +        meta = kwds.pop('metaclass')
      +    else:
      +        if bases:
      +            meta = type(bases[0])
      +        else:
      +            meta = type
      +    if isinstance(meta, type):
      +        # when meta is a type, we first determine the most-derived metaclass
      +        # instead of invoking the initial candidate directly
      +        meta = _calculate_meta(meta, bases)
      +    if hasattr(meta, '__prepare__'):
      +        ns = meta.__prepare__(name, bases, **kwds)
      +    else:
      +        ns = {}
      +    return meta, ns, kwds
      +
      +def _calculate_meta(meta, bases):
      +    """Calculate the most derived metaclass."""
      +    winner = meta
      +    for base in bases:
      +        base_meta = type(base)
      +        if issubclass(winner, base_meta):
      +            continue
      +        if issubclass(base_meta, winner):
      +            winner = base_meta
      +            continue
      +        # else:
      +        raise TypeError("metaclass conflict: "
      +                        "the metaclass of a derived class "
      +                        "must be a (non-strict) subclass "
      +                        "of the metaclasses of all its bases")
      +    return winner
      +
      +class DynamicClassAttribute:
      +    """Route attribute access on a class to __getattr__.
      +
      +    This is a descriptor, used to define attributes that act differently when
      +    accessed through an instance and through a class.  Instance access remains
      +    normal, but access to an attribute through a class will be routed to the
      +    class's __getattr__ method; this is done by raising AttributeError.
      +
      +    This allows one to have properties active on an instance, and have virtual
      +    attributes on the class with the same name (see Enum for an example).
      +
      +    """
      +    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
      +        self.fget = fget
      +        self.fset = fset
      +        self.fdel = fdel
      +        # next two lines make DynamicClassAttribute act the same as property
      +        self.__doc__ = doc or fget.__doc__
      +        self.overwrite_doc = doc is None
      +        # support for abstract methods
      +        self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False))
      +
      +    def __get__(self, instance, ownerclass=None):
      +        if instance is None:
      +            if self.__isabstractmethod__:
      +                return self
      +            raise AttributeError()
      +        elif self.fget is None:
      +            raise AttributeError("unreadable attribute")
      +        return self.fget(instance)
      +
      +    def __set__(self, instance, value):
      +        if self.fset is None:
      +            raise AttributeError("can't set attribute")
      +        self.fset(instance, value)
      +
      +    def __delete__(self, instance):
      +        if self.fdel is None:
      +            raise AttributeError("can't delete attribute")
      +        self.fdel(instance)
      +
      +    def getter(self, fget):
      +        fdoc = fget.__doc__ if self.overwrite_doc else None
      +        result = type(self)(fget, self.fset, self.fdel, fdoc or self.__doc__)
      +        result.overwrite_doc = self.overwrite_doc
      +        return result
      +
      +    def setter(self, fset):
      +        result = type(self)(self.fget, fset, self.fdel, self.__doc__)
      +        result.overwrite_doc = self.overwrite_doc
      +        return result
      +
      +    def deleter(self, fdel):
      +        result = type(self)(self.fget, self.fset, fdel, self.__doc__)
      +        result.overwrite_doc = self.overwrite_doc
      +        return result
      +
      +
      +import functools as _functools
      +import collections.abc as _collections_abc
      +
      +class _GeneratorWrapper:
      +    # TODO: Implement this in C.
      +    def __init__(self, gen):
      +        self.__wrapped = gen
      +        self.__isgen = gen.__class__ is GeneratorType
      +        self.__name__ = getattr(gen, '__name__', None)
      +        self.__qualname__ = getattr(gen, '__qualname__', None)
      +    def send(self, val):
      +        return self.__wrapped.send(val)
      +    def throw(self, tp, *rest):
      +        return self.__wrapped.throw(tp, *rest)
      +    def close(self):
      +        return self.__wrapped.close()
      +    @property
      +    def gi_code(self):
      +        return self.__wrapped.gi_code
      +    @property
      +    def gi_frame(self):
      +        return self.__wrapped.gi_frame
      +    @property
      +    def gi_running(self):
      +        return self.__wrapped.gi_running
      +    @property
      +    def gi_yieldfrom(self):
      +        return self.__wrapped.gi_yieldfrom
      +    cr_code = gi_code
      +    cr_frame = gi_frame
      +    cr_running = gi_running
      +    cr_await = gi_yieldfrom
      +    def __next__(self):
      +        return next(self.__wrapped)
      +    def __iter__(self):
      +        if self.__isgen:
      +            return self.__wrapped
      +        return self
      +    __await__ = __iter__
      +
      +def coroutine(func):
      +    """Convert regular generator function to a coroutine."""
      +
      +    if not callable(func):
      +        raise TypeError('types.coroutine() expects a callable')
      +
      +    if (func.__class__ is FunctionType and
      +        getattr(func, '__code__', None).__class__ is CodeType):
      +
      +        co_flags = func.__code__.co_flags
      +
      +        # Check if 'func' is a coroutine function.
      +        # (0x180 == CO_COROUTINE | CO_ITERABLE_COROUTINE)
      +        if co_flags & 0x180:
      +            return func
      +
      +        # Check if 'func' is a generator function.
      +        # (0x20 == CO_GENERATOR)
      +        if co_flags & 0x20:
      +            # TODO: Implement this in C.
      +            co = func.__code__
      +            func.__code__ = CodeType(
      +                co.co_argcount, co.co_kwonlyargcount, co.co_nlocals,
      +                co.co_stacksize,
      +                co.co_flags | 0x100,  # 0x100 == CO_ITERABLE_COROUTINE
      +                co.co_code,
      +                co.co_consts, co.co_names, co.co_varnames, co.co_filename,
      +                co.co_name, co.co_firstlineno, co.co_lnotab, co.co_freevars,
      +                co.co_cellvars)
      +            return func
      +
      +    # The following code is primarily to support functions that
      +    # return generator-like objects (for instance generators
      +    # compiled with Cython).
      +
      +    @_functools.wraps(func)
      +    def wrapped(*args, **kwargs):
      +        coro = func(*args, **kwargs)
      +        if (coro.__class__ is CoroutineType or
      +            coro.__class__ is GeneratorType and coro.gi_code.co_flags & 0x100):
      +            # 'coro' is a native coroutine object or an iterable coroutine
      +            return coro
      +        if (isinstance(coro, _collections_abc.Generator) and
      +            not isinstance(coro, _collections_abc.Coroutine)):
      +            # 'coro' is either a pure Python generator iterator, or it
      +            # implements collections.abc.Generator (and does not implement
      +            # collections.abc.Coroutine).
      +            return _GeneratorWrapper(coro)
      +        # 'coro' is either an instance of collections.abc.Coroutine or
      +        # some other object -- pass it through.
      +        return coro
      +
      +    return wrapped
      +
      +
      +__all__ = [n for n in globals() if n[:1] != '_']
      diff --git a/src/image_occlusion_enhanced/libaddon/_vendor_legacy/typing.py b/src/image_occlusion_enhanced/libaddon/_vendor_legacy/typing.py
      new file mode 100644
      index 00000000..f2b6aaf3
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/_vendor_legacy/typing.py
      @@ -0,0 +1,2412 @@
      +import abc
      +from abc import abstractmethod, abstractproperty
      +import collections
      +import contextlib
      +import functools
      +import re as stdlib_re  # Avoid confusion with the re we export.
      +import sys
      +import types
      +try:
      +    import collections.abc as collections_abc
      +except ImportError:
      +    import collections as collections_abc  # Fallback for PY3.2.
      +if sys.version_info[:2] >= (3, 6):
      +    import _collections_abc  # Needed for private function _check_methods # noqa
      +try:
      +    from types import WrapperDescriptorType, MethodWrapperType, MethodDescriptorType
      +except ImportError:
      +    WrapperDescriptorType = type(object.__init__)
      +    MethodWrapperType = type(object().__str__)
      +    MethodDescriptorType = type(str.join)
      +
      +
      +# Please keep __all__ alphabetized within each category.
      +__all__ = [
      +    # Super-special typing primitives.
      +    'Any',
      +    'Callable',
      +    'ClassVar',
      +    'Generic',
      +    'Optional',
      +    'Tuple',
      +    'Type',
      +    'TypeVar',
      +    'Union',
      +
      +    # ABCs (from collections.abc).
      +    'AbstractSet',  # collections.abc.Set.
      +    'GenericMeta',  # subclass of abc.ABCMeta and a metaclass
      +                    # for 'Generic' and ABCs below.
      +    'ByteString',
      +    'Container',
      +    'ContextManager',
      +    'Hashable',
      +    'ItemsView',
      +    'Iterable',
      +    'Iterator',
      +    'KeysView',
      +    'Mapping',
      +    'MappingView',
      +    'MutableMapping',
      +    'MutableSequence',
      +    'MutableSet',
      +    'Sequence',
      +    'Sized',
      +    'ValuesView',
      +    # The following are added depending on presence
      +    # of their non-generic counterparts in stdlib:
      +    # Awaitable,
      +    # AsyncIterator,
      +    # AsyncIterable,
      +    # Coroutine,
      +    # Collection,
      +    # AsyncGenerator,
      +    # AsyncContextManager
      +
      +    # Structural checks, a.k.a. protocols.
      +    'Reversible',
      +    'SupportsAbs',
      +    'SupportsBytes',
      +    'SupportsComplex',
      +    'SupportsFloat',
      +    'SupportsInt',
      +    'SupportsRound',
      +
      +    # Concrete collection types.
      +    'Counter',
      +    'Deque',
      +    'Dict',
      +    'DefaultDict',
      +    'List',
      +    'Set',
      +    'FrozenSet',
      +    'NamedTuple',  # Not really a type.
      +    'Generator',
      +
      +    # One-off things.
      +    'AnyStr',
      +    'cast',
      +    'get_type_hints',
      +    'NewType',
      +    'no_type_check',
      +    'no_type_check_decorator',
      +    'overload',
      +    'Text',
      +    'TYPE_CHECKING',
      +]
      +
      +# The pseudo-submodules 're' and 'io' are part of the public
      +# namespace, but excluded from __all__ because they might stomp on
      +# legitimate imports of those modules.
      +
      +
      +def _qualname(x):
      +    if sys.version_info[:2] >= (3, 3):
      +        return x.__qualname__
      +    else:
      +        # Fall back to just name.
      +        return x.__name__
      +
      +
      +def _trim_name(nm):
      +    whitelist = ('_TypeAlias', '_ForwardRef', '_TypingBase', '_FinalTypingBase')
      +    if nm.startswith('_') and nm not in whitelist:
      +        nm = nm[1:]
      +    return nm
      +
      +
      +class TypingMeta(type):
      +    """Metaclass for most types defined in typing module
      +    (not a part of public API).
      +
      +    This overrides __new__() to require an extra keyword parameter
      +    '_root', which serves as a guard against naive subclassing of the
      +    typing classes.  Any legitimate class defined using a metaclass
      +    derived from TypingMeta must pass _root=True.
      +
      +    This also defines a dummy constructor (all the work for most typing
      +    constructs is done in __new__) and a nicer repr().
      +    """
      +
      +    _is_protocol = False
      +
      +    def __new__(cls, name, bases, namespace, *, _root=False):
      +        if not _root:
      +            raise TypeError("Cannot subclass %s" %
      +                            (', '.join(map(_type_repr, bases)) or '()'))
      +        return super().__new__(cls, name, bases, namespace)
      +
      +    def __init__(self, *args, **kwds):
      +        pass
      +
      +    def _eval_type(self, globalns, localns):
      +        """Override this in subclasses to interpret forward references.
      +
      +        For example, List['C'] is internally stored as
      +        List[_ForwardRef('C')], which should evaluate to List[C],
      +        where C is an object found in globalns or localns (searching
      +        localns first, of course).
      +        """
      +        return self
      +
      +    def _get_type_vars(self, tvars):
      +        pass
      +
      +    def __repr__(self):
      +        qname = _trim_name(_qualname(self))
      +        return '%s.%s' % (self.__module__, qname)
      +
      +
      +class _TypingBase(metaclass=TypingMeta, _root=True):
      +    """Internal indicator of special typing constructs."""
      +
      +    __slots__ = ('__weakref__',)
      +
      +    def __init__(self, *args, **kwds):
      +        pass
      +
      +    def __new__(cls, *args, **kwds):
      +        """Constructor.
      +
      +        This only exists to give a better error message in case
      +        someone tries to subclass a special typing object (not a good idea).
      +        """
      +        if (len(args) == 3 and
      +                isinstance(args[0], str) and
      +                isinstance(args[1], tuple)):
      +            # Close enough.
      +            raise TypeError("Cannot subclass %r" % cls)
      +        return super().__new__(cls)
      +
      +    # Things that are not classes also need these.
      +    def _eval_type(self, globalns, localns):
      +        return self
      +
      +    def _get_type_vars(self, tvars):
      +        pass
      +
      +    def __repr__(self):
      +        cls = type(self)
      +        qname = _trim_name(_qualname(cls))
      +        return '%s.%s' % (cls.__module__, qname)
      +
      +    def __call__(self, *args, **kwds):
      +        raise TypeError("Cannot instantiate %r" % type(self))
      +
      +
      +class _FinalTypingBase(_TypingBase, _root=True):
      +    """Internal mix-in class to prevent instantiation.
      +
      +    Prevents instantiation unless _root=True is given in class call.
      +    It is used to create pseudo-singleton instances Any, Union, Optional, etc.
      +    """
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, _root=False, **kwds):
      +        self = super().__new__(cls, *args, **kwds)
      +        if _root is True:
      +            return self
      +        raise TypeError("Cannot instantiate %r" % cls)
      +
      +    def __reduce__(self):
      +        return _trim_name(type(self).__name__)
      +
      +
      +class _ForwardRef(_TypingBase, _root=True):
      +    """Internal wrapper to hold a forward reference."""
      +
      +    __slots__ = ('__forward_arg__', '__forward_code__',
      +                 '__forward_evaluated__', '__forward_value__')
      +
      +    def __init__(self, arg):
      +        super().__init__(arg)
      +        if not isinstance(arg, str):
      +            raise TypeError('Forward reference must be a string -- got %r' % (arg,))
      +        try:
      +            code = compile(arg, '', 'eval')
      +        except SyntaxError:
      +            raise SyntaxError('Forward reference must be an expression -- got %r' %
      +                              (arg,))
      +        self.__forward_arg__ = arg
      +        self.__forward_code__ = code
      +        self.__forward_evaluated__ = False
      +        self.__forward_value__ = None
      +
      +    def _eval_type(self, globalns, localns):
      +        if not self.__forward_evaluated__ or localns is not globalns:
      +            if globalns is None and localns is None:
      +                globalns = localns = {}
      +            elif globalns is None:
      +                globalns = localns
      +            elif localns is None:
      +                localns = globalns
      +            self.__forward_value__ = _type_check(
      +                eval(self.__forward_code__, globalns, localns),
      +                "Forward references must evaluate to types.")
      +            self.__forward_evaluated__ = True
      +        return self.__forward_value__
      +
      +    def __eq__(self, other):
      +        if not isinstance(other, _ForwardRef):
      +            return NotImplemented
      +        return (self.__forward_arg__ == other.__forward_arg__ and
      +                self.__forward_value__ == other.__forward_value__)
      +
      +    def __hash__(self):
      +        return hash((self.__forward_arg__, self.__forward_value__))
      +
      +    def __instancecheck__(self, obj):
      +        raise TypeError("Forward references cannot be used with isinstance().")
      +
      +    def __subclasscheck__(self, cls):
      +        raise TypeError("Forward references cannot be used with issubclass().")
      +
      +    def __repr__(self):
      +        return '_ForwardRef(%r)' % (self.__forward_arg__,)
      +
      +
      +class _TypeAlias(_TypingBase, _root=True):
      +    """Internal helper class for defining generic variants of concrete types.
      +
      +    Note that this is not a type; let's call it a pseudo-type.  It cannot
      +    be used in instance and subclass checks in parameterized form, i.e.
      +    ``isinstance(42, Match[str])`` raises ``TypeError`` instead of returning
      +    ``False``.
      +    """
      +
      +    __slots__ = ('name', 'type_var', 'impl_type', 'type_checker')
      +
      +    def __init__(self, name, type_var, impl_type, type_checker):
      +        """Initializer.
      +
      +        Args:
      +            name: The name, e.g. 'Pattern'.
      +            type_var: The type parameter, e.g. AnyStr, or the
      +                specific type, e.g. str.
      +            impl_type: The implementation type.
      +            type_checker: Function that takes an impl_type instance.
      +                and returns a value that should be a type_var instance.
      +        """
      +        assert isinstance(name, str), repr(name)
      +        assert isinstance(impl_type, type), repr(impl_type)
      +        assert not isinstance(impl_type, TypingMeta), repr(impl_type)
      +        assert isinstance(type_var, (type, _TypingBase)), repr(type_var)
      +        self.name = name
      +        self.type_var = type_var
      +        self.impl_type = impl_type
      +        self.type_checker = type_checker
      +
      +    def __repr__(self):
      +        return "%s[%s]" % (self.name, _type_repr(self.type_var))
      +
      +    def __getitem__(self, parameter):
      +        if not isinstance(self.type_var, TypeVar):
      +            raise TypeError("%s cannot be further parameterized." % self)
      +        if self.type_var.__constraints__ and isinstance(parameter, type):
      +            if not issubclass(parameter, self.type_var.__constraints__):
      +                raise TypeError("%s is not a valid substitution for %s." %
      +                                (parameter, self.type_var))
      +        if isinstance(parameter, TypeVar) and parameter is not self.type_var:
      +            raise TypeError("%s cannot be re-parameterized." % self)
      +        return self.__class__(self.name, parameter,
      +                              self.impl_type, self.type_checker)
      +
      +    def __eq__(self, other):
      +        if not isinstance(other, _TypeAlias):
      +            return NotImplemented
      +        return self.name == other.name and self.type_var == other.type_var
      +
      +    def __hash__(self):
      +        return hash((self.name, self.type_var))
      +
      +    def __instancecheck__(self, obj):
      +        if not isinstance(self.type_var, TypeVar):
      +            raise TypeError("Parameterized type aliases cannot be used "
      +                            "with isinstance().")
      +        return isinstance(obj, self.impl_type)
      +
      +    def __subclasscheck__(self, cls):
      +        if not isinstance(self.type_var, TypeVar):
      +            raise TypeError("Parameterized type aliases cannot be used "
      +                            "with issubclass().")
      +        return issubclass(cls, self.impl_type)
      +
      +
      +def _get_type_vars(types, tvars):
      +    for t in types:
      +        if isinstance(t, TypingMeta) or isinstance(t, _TypingBase):
      +            t._get_type_vars(tvars)
      +
      +
      +def _type_vars(types):
      +    tvars = []
      +    _get_type_vars(types, tvars)
      +    return tuple(tvars)
      +
      +
      +def _eval_type(t, globalns, localns):
      +    if isinstance(t, TypingMeta) or isinstance(t, _TypingBase):
      +        return t._eval_type(globalns, localns)
      +    return t
      +
      +
      +def _type_check(arg, msg):
      +    """Check that the argument is a type, and return it (internal helper).
      +
      +    As a special case, accept None and return type(None) instead.
      +    Also, _TypeAlias instances (e.g. Match, Pattern) are acceptable.
      +
      +    The msg argument is a human-readable error message, e.g.
      +
      +        "Union[arg, ...]: arg should be a type."
      +
      +    We append the repr() of the actual value (truncated to 100 chars).
      +    """
      +    if arg is None:
      +        return type(None)
      +    if isinstance(arg, str):
      +        arg = _ForwardRef(arg)
      +    if (
      +        isinstance(arg, _TypingBase) and type(arg).__name__ == '_ClassVar' or
      +        not isinstance(arg, (type, _TypingBase)) and not callable(arg)
      +    ):
      +        raise TypeError(msg + " Got %.100r." % (arg,))
      +    # Bare Union etc. are not valid as type arguments
      +    if (
      +        type(arg).__name__ in ('_Union', '_Optional') and
      +        not getattr(arg, '__origin__', None) or
      +        isinstance(arg, TypingMeta) and arg._gorg in (Generic, _Protocol)
      +    ):
      +        raise TypeError("Plain %s is not valid as type argument" % arg)
      +    return arg
      +
      +
      +def _type_repr(obj):
      +    """Return the repr() of an object, special-casing types (internal helper).
      +
      +    If obj is a type, we return a shorter version than the default
      +    type.__repr__, based on the module and qualified name, which is
      +    typically enough to uniquely identify a type.  For everything
      +    else, we fall back on repr(obj).
      +    """
      +    if isinstance(obj, type) and not isinstance(obj, TypingMeta):
      +        if obj.__module__ == 'builtins':
      +            return _qualname(obj)
      +        return '%s.%s' % (obj.__module__, _qualname(obj))
      +    if obj is ...:
      +        return('...')
      +    if isinstance(obj, types.FunctionType):
      +        return obj.__name__
      +    return repr(obj)
      +
      +
      +class _Any(_FinalTypingBase, _root=True):
      +    """Special type indicating an unconstrained type.
      +
      +    - Any is compatible with every type.
      +    - Any assumed to have all methods.
      +    - All values assumed to be instances of Any.
      +
      +    Note that all the above statements are true from the point of view of
      +    static type checkers. At runtime, Any should not be used with instance
      +    or class checks.
      +    """
      +
      +    __slots__ = ()
      +
      +    def __instancecheck__(self, obj):
      +        raise TypeError("Any cannot be used with isinstance().")
      +
      +    def __subclasscheck__(self, cls):
      +        raise TypeError("Any cannot be used with issubclass().")
      +
      +
      +Any = _Any(_root=True)
      +
      +
      +class _NoReturn(_FinalTypingBase, _root=True):
      +    """Special type indicating functions that never return.
      +    Example::
      +
      +      from typing import NoReturn
      +
      +      def stop() -> NoReturn:
      +          raise Exception('no way')
      +
      +    This type is invalid in other positions, e.g., ``List[NoReturn]``
      +    will fail in static type checkers.
      +    """
      +
      +    __slots__ = ()
      +
      +    def __instancecheck__(self, obj):
      +        raise TypeError("NoReturn cannot be used with isinstance().")
      +
      +    def __subclasscheck__(self, cls):
      +        raise TypeError("NoReturn cannot be used with issubclass().")
      +
      +
      +NoReturn = _NoReturn(_root=True)
      +
      +
      +class TypeVar(_TypingBase, _root=True):
      +    """Type variable.
      +
      +    Usage::
      +
      +      T = TypeVar('T')  # Can be anything
      +      A = TypeVar('A', str, bytes)  # Must be str or bytes
      +
      +    Type variables exist primarily for the benefit of static type
      +    checkers.  They serve as the parameters for generic types as well
      +    as for generic function definitions.  See class Generic for more
      +    information on generic types.  Generic functions work as follows:
      +
      +      def repeat(x: T, n: int) -> List[T]:
      +          '''Return a list containing n references to x.'''
      +          return [x]*n
      +
      +      def longest(x: A, y: A) -> A:
      +          '''Return the longest of two strings.'''
      +          return x if len(x) >= len(y) else y
      +
      +    The latter example's signature is essentially the overloading
      +    of (str, str) -> str and (bytes, bytes) -> bytes.  Also note
      +    that if the arguments are instances of some subclass of str,
      +    the return type is still plain str.
      +
      +    At runtime, isinstance(x, T) and issubclass(C, T) will raise TypeError.
      +
      +    Type variables defined with covariant=True or contravariant=True
      +    can be used do declare covariant or contravariant generic types.
      +    See PEP 484 for more details. By default generic types are invariant
      +    in all type variables.
      +
      +    Type variables can be introspected. e.g.:
      +
      +      T.__name__ == 'T'
      +      T.__constraints__ == ()
      +      T.__covariant__ == False
      +      T.__contravariant__ = False
      +      A.__constraints__ == (str, bytes)
      +    """
      +
      +    __slots__ = ('__name__', '__bound__', '__constraints__',
      +                 '__covariant__', '__contravariant__')
      +
      +    def __init__(self, name, *constraints, bound=None,
      +                 covariant=False, contravariant=False):
      +        super().__init__(name, *constraints, bound=bound,
      +                         covariant=covariant, contravariant=contravariant)
      +        self.__name__ = name
      +        if covariant and contravariant:
      +            raise ValueError("Bivariant types are not supported.")
      +        self.__covariant__ = bool(covariant)
      +        self.__contravariant__ = bool(contravariant)
      +        if constraints and bound is not None:
      +            raise TypeError("Constraints cannot be combined with bound=...")
      +        if constraints and len(constraints) == 1:
      +            raise TypeError("A single constraint is not allowed")
      +        msg = "TypeVar(name, constraint, ...): constraints must be types."
      +        self.__constraints__ = tuple(_type_check(t, msg) for t in constraints)
      +        if bound:
      +            self.__bound__ = _type_check(bound, "Bound must be a type.")
      +        else:
      +            self.__bound__ = None
      +
      +    def _get_type_vars(self, tvars):
      +        if self not in tvars:
      +            tvars.append(self)
      +
      +    def __repr__(self):
      +        if self.__covariant__:
      +            prefix = '+'
      +        elif self.__contravariant__:
      +            prefix = '-'
      +        else:
      +            prefix = '~'
      +        return prefix + self.__name__
      +
      +    def __instancecheck__(self, instance):
      +        raise TypeError("Type variables cannot be used with isinstance().")
      +
      +    def __subclasscheck__(self, cls):
      +        raise TypeError("Type variables cannot be used with issubclass().")
      +
      +
      +# Some unconstrained type variables.  These are used by the container types.
      +# (These are not for export.)
      +T = TypeVar('T')  # Any type.
      +KT = TypeVar('KT')  # Key type.
      +VT = TypeVar('VT')  # Value type.
      +T_co = TypeVar('T_co', covariant=True)  # Any type covariant containers.
      +V_co = TypeVar('V_co', covariant=True)  # Any type covariant containers.
      +VT_co = TypeVar('VT_co', covariant=True)  # Value type covariant containers.
      +T_contra = TypeVar('T_contra', contravariant=True)  # Ditto contravariant.
      +
      +# A useful type variable with constraints.  This represents string types.
      +# (This one *is* for export!)
      +AnyStr = TypeVar('AnyStr', bytes, str)
      +
      +
      +def _replace_arg(arg, tvars, args):
      +    """An internal helper function: replace arg if it is a type variable
      +    found in tvars with corresponding substitution from args or
      +    with corresponding substitution sub-tree if arg is a generic type.
      +    """
      +
      +    if tvars is None:
      +        tvars = []
      +    if hasattr(arg, '_subs_tree') and isinstance(arg, (GenericMeta, _TypingBase)):
      +        return arg._subs_tree(tvars, args)
      +    if isinstance(arg, TypeVar):
      +        for i, tvar in enumerate(tvars):
      +            if arg == tvar:
      +                return args[i]
      +    return arg
      +
      +
      +# Special typing constructs Union, Optional, Generic, Callable and Tuple
      +# use three special attributes for internal bookkeeping of generic types:
      +# * __parameters__ is a tuple of unique free type parameters of a generic
      +#   type, for example, Dict[T, T].__parameters__ == (T,);
      +# * __origin__ keeps a reference to a type that was subscripted,
      +#   e.g., Union[T, int].__origin__ == Union;
      +# * __args__ is a tuple of all arguments used in subscripting,
      +#   e.g., Dict[T, int].__args__ == (T, int).
      +
      +
      +def _subs_tree(cls, tvars=None, args=None):
      +    """An internal helper function: calculate substitution tree
      +    for generic cls after replacing its type parameters with
      +    substitutions in tvars -> args (if any).
      +    Repeat the same following __origin__'s.
      +
      +    Return a list of arguments with all possible substitutions
      +    performed. Arguments that are generic classes themselves are represented
      +    as tuples (so that no new classes are created by this function).
      +    For example: _subs_tree(List[Tuple[int, T]][str]) == [(Tuple, int, str)]
      +    """
      +
      +    if cls.__origin__ is None:
      +        return cls
      +    # Make of chain of origins (i.e. cls -> cls.__origin__)
      +    current = cls.__origin__
      +    orig_chain = []
      +    while current.__origin__ is not None:
      +        orig_chain.append(current)
      +        current = current.__origin__
      +    # Replace type variables in __args__ if asked ...
      +    tree_args = []
      +    for arg in cls.__args__:
      +        tree_args.append(_replace_arg(arg, tvars, args))
      +    # ... then continue replacing down the origin chain.
      +    for ocls in orig_chain:
      +        new_tree_args = []
      +        for arg in ocls.__args__:
      +            new_tree_args.append(_replace_arg(arg, ocls.__parameters__, tree_args))
      +        tree_args = new_tree_args
      +    return tree_args
      +
      +
      +def _remove_dups_flatten(parameters):
      +    """An internal helper for Union creation and substitution: flatten Union's
      +    among parameters, then remove duplicates and strict subclasses.
      +    """
      +
      +    # Flatten out Union[Union[...], ...].
      +    params = []
      +    for p in parameters:
      +        if isinstance(p, _Union) and p.__origin__ is Union:
      +            params.extend(p.__args__)
      +        elif isinstance(p, tuple) and len(p) > 0 and p[0] is Union:
      +            params.extend(p[1:])
      +        else:
      +            params.append(p)
      +    # Weed out strict duplicates, preserving the first of each occurrence.
      +    all_params = set(params)
      +    if len(all_params) < len(params):
      +        new_params = []
      +        for t in params:
      +            if t in all_params:
      +                new_params.append(t)
      +                all_params.remove(t)
      +        params = new_params
      +        assert not all_params, all_params
      +    # Weed out subclasses.
      +    # E.g. Union[int, Employee, Manager] == Union[int, Employee].
      +    # If object is present it will be sole survivor among proper classes.
      +    # Never discard type variables.
      +    # (In particular, Union[str, AnyStr] != AnyStr.)
      +    all_params = set(params)
      +    for t1 in params:
      +        if not isinstance(t1, type):
      +            continue
      +        if any(isinstance(t2, type) and issubclass(t1, t2)
      +               for t2 in all_params - {t1}
      +               if not (isinstance(t2, GenericMeta) and
      +                       t2.__origin__ is not None)):
      +            all_params.remove(t1)
      +    return tuple(t for t in params if t in all_params)
      +
      +
      +def _check_generic(cls, parameters):
      +    # Check correct count for parameters of a generic cls (internal helper).
      +    if not cls.__parameters__:
      +        raise TypeError("%s is not a generic class" % repr(cls))
      +    alen = len(parameters)
      +    elen = len(cls.__parameters__)
      +    if alen != elen:
      +        raise TypeError("Too %s parameters for %s; actual %s, expected %s" %
      +                        ("many" if alen > elen else "few", repr(cls), alen, elen))
      +
      +
      +_cleanups = []
      +
      +
      +def _tp_cache(func):
      +    """Internal wrapper caching __getitem__ of generic types with a fallback to
      +    original function for non-hashable arguments.
      +    """
      +
      +    cached = functools.lru_cache()(func)
      +    _cleanups.append(cached.cache_clear)
      +
      +    @functools.wraps(func)
      +    def inner(*args, **kwds):
      +        try:
      +            return cached(*args, **kwds)
      +        except TypeError:
      +            pass  # All real errors (not unhashable args) are raised below.
      +        return func(*args, **kwds)
      +    return inner
      +
      +
      +class _Union(_FinalTypingBase, _root=True):
      +    """Union type; Union[X, Y] means either X or Y.
      +
      +    To define a union, use e.g. Union[int, str].  Details:
      +
      +    - The arguments must be types and there must be at least one.
      +
      +    - None as an argument is a special case and is replaced by
      +      type(None).
      +
      +    - Unions of unions are flattened, e.g.::
      +
      +        Union[Union[int, str], float] == Union[int, str, float]
      +
      +    - Unions of a single argument vanish, e.g.::
      +
      +        Union[int] == int  # The constructor actually returns int
      +
      +    - Redundant arguments are skipped, e.g.::
      +
      +        Union[int, str, int] == Union[int, str]
      +
      +    - When comparing unions, the argument order is ignored, e.g.::
      +
      +        Union[int, str] == Union[str, int]
      +
      +    - When two arguments have a subclass relationship, the least
      +      derived argument is kept, e.g.::
      +
      +        class Employee: pass
      +        class Manager(Employee): pass
      +        Union[int, Employee, Manager] == Union[int, Employee]
      +        Union[Manager, int, Employee] == Union[int, Employee]
      +        Union[Employee, Manager] == Employee
      +
      +    - Similar for object::
      +
      +        Union[int, object] == object
      +
      +    - You cannot subclass or instantiate a union.
      +
      +    - You can use Optional[X] as a shorthand for Union[X, None].
      +    """
      +
      +    __slots__ = ('__parameters__', '__args__', '__origin__', '__tree_hash__')
      +
      +    def __new__(cls, parameters=None, origin=None, *args, _root=False):
      +        self = super().__new__(cls, parameters, origin, *args, _root=_root)
      +        if origin is None:
      +            self.__parameters__ = None
      +            self.__args__ = None
      +            self.__origin__ = None
      +            self.__tree_hash__ = hash(frozenset(('Union',)))
      +            return self
      +        if not isinstance(parameters, tuple):
      +            raise TypeError("Expected parameters=")
      +        if origin is Union:
      +            parameters = _remove_dups_flatten(parameters)
      +            # It's not a union if there's only one type left.
      +            if len(parameters) == 1:
      +                return parameters[0]
      +        self.__parameters__ = _type_vars(parameters)
      +        self.__args__ = parameters
      +        self.__origin__ = origin
      +        # Pre-calculate the __hash__ on instantiation.
      +        # This improves speed for complex substitutions.
      +        subs_tree = self._subs_tree()
      +        if isinstance(subs_tree, tuple):
      +            self.__tree_hash__ = hash(frozenset(subs_tree))
      +        else:
      +            self.__tree_hash__ = hash(subs_tree)
      +        return self
      +
      +    def _eval_type(self, globalns, localns):
      +        if self.__args__ is None:
      +            return self
      +        ev_args = tuple(_eval_type(t, globalns, localns) for t in self.__args__)
      +        ev_origin = _eval_type(self.__origin__, globalns, localns)
      +        if ev_args == self.__args__ and ev_origin == self.__origin__:
      +            # Everything is already evaluated.
      +            return self
      +        return self.__class__(ev_args, ev_origin, _root=True)
      +
      +    def _get_type_vars(self, tvars):
      +        if self.__origin__ and self.__parameters__:
      +            _get_type_vars(self.__parameters__, tvars)
      +
      +    def __repr__(self):
      +        if self.__origin__ is None:
      +            return super().__repr__()
      +        tree = self._subs_tree()
      +        if not isinstance(tree, tuple):
      +            return repr(tree)
      +        return tree[0]._tree_repr(tree)
      +
      +    def _tree_repr(self, tree):
      +        arg_list = []
      +        for arg in tree[1:]:
      +            if not isinstance(arg, tuple):
      +                arg_list.append(_type_repr(arg))
      +            else:
      +                arg_list.append(arg[0]._tree_repr(arg))
      +        return super().__repr__() + '[%s]' % ', '.join(arg_list)
      +
      +    @_tp_cache
      +    def __getitem__(self, parameters):
      +        if parameters == ():
      +            raise TypeError("Cannot take a Union of no types.")
      +        if not isinstance(parameters, tuple):
      +            parameters = (parameters,)
      +        if self.__origin__ is None:
      +            msg = "Union[arg, ...]: each arg must be a type."
      +        else:
      +            msg = "Parameters to generic types must be types."
      +        parameters = tuple(_type_check(p, msg) for p in parameters)
      +        if self is not Union:
      +            _check_generic(self, parameters)
      +        return self.__class__(parameters, origin=self, _root=True)
      +
      +    def _subs_tree(self, tvars=None, args=None):
      +        if self is Union:
      +            return Union  # Nothing to substitute
      +        tree_args = _subs_tree(self, tvars, args)
      +        tree_args = _remove_dups_flatten(tree_args)
      +        if len(tree_args) == 1:
      +            return tree_args[0]  # Union of a single type is that type
      +        return (Union,) + tree_args
      +
      +    def __eq__(self, other):
      +        if isinstance(other, _Union):
      +            return self.__tree_hash__ == other.__tree_hash__
      +        elif self is not Union:
      +            return self._subs_tree() == other
      +        else:
      +            return self is other
      +
      +    def __hash__(self):
      +        return self.__tree_hash__
      +
      +    def __instancecheck__(self, obj):
      +        raise TypeError("Unions cannot be used with isinstance().")
      +
      +    def __subclasscheck__(self, cls):
      +        raise TypeError("Unions cannot be used with issubclass().")
      +
      +
      +Union = _Union(_root=True)
      +
      +
      +class _Optional(_FinalTypingBase, _root=True):
      +    """Optional type.
      +
      +    Optional[X] is equivalent to Union[X, None].
      +    """
      +
      +    __slots__ = ()
      +
      +    @_tp_cache
      +    def __getitem__(self, arg):
      +        arg = _type_check(arg, "Optional[t] requires a single type.")
      +        return Union[arg, type(None)]
      +
      +
      +Optional = _Optional(_root=True)
      +
      +
      +def _next_in_mro(cls):
      +    """Helper for Generic.__new__.
      +
      +    Returns the class after the last occurrence of Generic or
      +    Generic[...] in cls.__mro__.
      +    """
      +    next_in_mro = object
      +    # Look for the last occurrence of Generic or Generic[...].
      +    for i, c in enumerate(cls.__mro__[:-1]):
      +        if isinstance(c, GenericMeta) and c._gorg is Generic:
      +            next_in_mro = cls.__mro__[i + 1]
      +    return next_in_mro
      +
      +
      +def _make_subclasshook(cls):
      +    """Construct a __subclasshook__ callable that incorporates
      +    the associated __extra__ class in subclass checks performed
      +    against cls.
      +    """
      +    if isinstance(cls.__extra__, abc.ABCMeta):
      +        # The logic mirrors that of ABCMeta.__subclasscheck__.
      +        # Registered classes need not be checked here because
      +        # cls and its extra share the same _abc_registry.
      +        def __extrahook__(subclass):
      +            res = cls.__extra__.__subclasshook__(subclass)
      +            if res is not NotImplemented:
      +                return res
      +            if cls.__extra__ in subclass.__mro__:
      +                return True
      +            for scls in cls.__extra__.__subclasses__():
      +                if isinstance(scls, GenericMeta):
      +                    continue
      +                if issubclass(subclass, scls):
      +                    return True
      +            return NotImplemented
      +    else:
      +        # For non-ABC extras we'll just call issubclass().
      +        def __extrahook__(subclass):
      +            if cls.__extra__ and issubclass(subclass, cls.__extra__):
      +                return True
      +            return NotImplemented
      +    return __extrahook__
      +
      +
      +def _no_slots_copy(dct):
      +    """Internal helper: copy class __dict__ and clean slots class variables.
      +    (They will be re-created if necessary by normal class machinery.)
      +    """
      +    dict_copy = dict(dct)
      +    if '__slots__' in dict_copy:
      +        for slot in dict_copy['__slots__']:
      +            dict_copy.pop(slot, None)
      +    return dict_copy
      +
      +
      +class GenericMeta(TypingMeta, abc.ABCMeta):
      +    """Metaclass for generic types.
      +
      +    This is a metaclass for typing.Generic and generic ABCs defined in
      +    typing module. User defined subclasses of GenericMeta can override
      +    __new__ and invoke super().__new__. Note that GenericMeta.__new__
      +    has strict rules on what is allowed in its bases argument:
      +    * plain Generic is disallowed in bases;
      +    * Generic[...] should appear in bases at most once;
      +    * if Generic[...] is present, then it should list all type variables
      +      that appear in other bases.
      +    In addition, type of all generic bases is erased, e.g., C[int] is
      +    stripped to plain C.
      +    """
      +
      +    def __new__(cls, name, bases, namespace,
      +                tvars=None, args=None, origin=None, extra=None, orig_bases=None):
      +        """Create a new generic class. GenericMeta.__new__ accepts
      +        keyword arguments that are used for internal bookkeeping, therefore
      +        an override should pass unused keyword arguments to super().
      +        """
      +        if tvars is not None:
      +            # Called from __getitem__() below.
      +            assert origin is not None
      +            assert all(isinstance(t, TypeVar) for t in tvars), tvars
      +        else:
      +            # Called from class statement.
      +            assert tvars is None, tvars
      +            assert args is None, args
      +            assert origin is None, origin
      +
      +            # Get the full set of tvars from the bases.
      +            tvars = _type_vars(bases)
      +            # Look for Generic[T1, ..., Tn].
      +            # If found, tvars must be a subset of it.
      +            # If not found, tvars is it.
      +            # Also check for and reject plain Generic,
      +            # and reject multiple Generic[...].
      +            gvars = None
      +            for base in bases:
      +                if base is Generic:
      +                    raise TypeError("Cannot inherit from plain Generic")
      +                if (isinstance(base, GenericMeta) and
      +                        base.__origin__ is Generic):
      +                    if gvars is not None:
      +                        raise TypeError(
      +                            "Cannot inherit from Generic[...] multiple types.")
      +                    gvars = base.__parameters__
      +            if gvars is None:
      +                gvars = tvars
      +            else:
      +                tvarset = set(tvars)
      +                gvarset = set(gvars)
      +                if not tvarset <= gvarset:
      +                    raise TypeError(
      +                        "Some type variables (%s) "
      +                        "are not listed in Generic[%s]" %
      +                        (", ".join(str(t) for t in tvars if t not in gvarset),
      +                         ", ".join(str(g) for g in gvars)))
      +                tvars = gvars
      +
      +        initial_bases = bases
      +        if extra is not None and type(extra) is abc.ABCMeta and extra not in bases:
      +            bases = (extra,) + bases
      +        bases = tuple(b._gorg if isinstance(b, GenericMeta) else b for b in bases)
      +
      +        # remove bare Generic from bases if there are other generic bases
      +        if any(isinstance(b, GenericMeta) and b is not Generic for b in bases):
      +            bases = tuple(b for b in bases if b is not Generic)
      +        namespace.update({'__origin__': origin, '__extra__': extra,
      +                          '_gorg': None if not origin else origin._gorg})
      +        self = super().__new__(cls, name, bases, namespace, _root=True)
      +        super(GenericMeta, self).__setattr__('_gorg',
      +                                             self if not origin else origin._gorg)
      +        self.__parameters__ = tvars
      +        # Be prepared that GenericMeta will be subclassed by TupleMeta
      +        # and CallableMeta, those two allow ..., (), or [] in __args___.
      +        self.__args__ = tuple(... if a is _TypingEllipsis else
      +                              () if a is _TypingEmpty else
      +                              a for a in args) if args else None
      +        # Speed hack (https://github.com/python/typing/issues/196).
      +        self.__next_in_mro__ = _next_in_mro(self)
      +        # Preserve base classes on subclassing (__bases__ are type erased now).
      +        if orig_bases is None:
      +            self.__orig_bases__ = initial_bases
      +
      +        # This allows unparameterized generic collections to be used
      +        # with issubclass() and isinstance() in the same way as their
      +        # collections.abc counterparts (e.g., isinstance([], Iterable)).
      +        if (
      +            '__subclasshook__' not in namespace and extra or
      +            # allow overriding
      +            getattr(self.__subclasshook__, '__name__', '') == '__extrahook__'
      +        ):
      +            self.__subclasshook__ = _make_subclasshook(self)
      +        if isinstance(extra, abc.ABCMeta):
      +            self._abc_registry = extra._abc_registry
      +            self._abc_cache = extra._abc_cache
      +        elif origin is not None:
      +            self._abc_registry = origin._abc_registry
      +            self._abc_cache = origin._abc_cache
      +
      +        if origin and hasattr(origin, '__qualname__'):  # Fix for Python 3.2.
      +            self.__qualname__ = origin.__qualname__
      +        self.__tree_hash__ = (hash(self._subs_tree()) if origin else
      +                              super(GenericMeta, self).__hash__())
      +        return self
      +
      +    # _abc_negative_cache and _abc_negative_cache_version
      +    # realised as descriptors, since GenClass[t1, t2, ...] always
      +    # share subclass info with GenClass.
      +    # This is an important memory optimization.
      +    @property
      +    def _abc_negative_cache(self):
      +        if isinstance(self.__extra__, abc.ABCMeta):
      +            return self.__extra__._abc_negative_cache
      +        return self._gorg._abc_generic_negative_cache
      +
      +    @_abc_negative_cache.setter
      +    def _abc_negative_cache(self, value):
      +        if self.__origin__ is None:
      +            if isinstance(self.__extra__, abc.ABCMeta):
      +                self.__extra__._abc_negative_cache = value
      +            else:
      +                self._abc_generic_negative_cache = value
      +
      +    @property
      +    def _abc_negative_cache_version(self):
      +        if isinstance(self.__extra__, abc.ABCMeta):
      +            return self.__extra__._abc_negative_cache_version
      +        return self._gorg._abc_generic_negative_cache_version
      +
      +    @_abc_negative_cache_version.setter
      +    def _abc_negative_cache_version(self, value):
      +        if self.__origin__ is None:
      +            if isinstance(self.__extra__, abc.ABCMeta):
      +                self.__extra__._abc_negative_cache_version = value
      +            else:
      +                self._abc_generic_negative_cache_version = value
      +
      +    def _get_type_vars(self, tvars):
      +        if self.__origin__ and self.__parameters__:
      +            _get_type_vars(self.__parameters__, tvars)
      +
      +    def _eval_type(self, globalns, localns):
      +        ev_origin = (self.__origin__._eval_type(globalns, localns)
      +                     if self.__origin__ else None)
      +        ev_args = tuple(_eval_type(a, globalns, localns) for a
      +                        in self.__args__) if self.__args__ else None
      +        if ev_origin == self.__origin__ and ev_args == self.__args__:
      +            return self
      +        return self.__class__(self.__name__,
      +                              self.__bases__,
      +                              _no_slots_copy(self.__dict__),
      +                              tvars=_type_vars(ev_args) if ev_args else None,
      +                              args=ev_args,
      +                              origin=ev_origin,
      +                              extra=self.__extra__,
      +                              orig_bases=self.__orig_bases__)
      +
      +    def __repr__(self):
      +        if self.__origin__ is None:
      +            return super().__repr__()
      +        return self._tree_repr(self._subs_tree())
      +
      +    def _tree_repr(self, tree):
      +        arg_list = []
      +        for arg in tree[1:]:
      +            if arg == ():
      +                arg_list.append('()')
      +            elif not isinstance(arg, tuple):
      +                arg_list.append(_type_repr(arg))
      +            else:
      +                arg_list.append(arg[0]._tree_repr(arg))
      +        return super().__repr__() + '[%s]' % ', '.join(arg_list)
      +
      +    def _subs_tree(self, tvars=None, args=None):
      +        if self.__origin__ is None:
      +            return self
      +        tree_args = _subs_tree(self, tvars, args)
      +        return (self._gorg,) + tuple(tree_args)
      +
      +    def __eq__(self, other):
      +        if not isinstance(other, GenericMeta):
      +            return NotImplemented
      +        if self.__origin__ is None or other.__origin__ is None:
      +            return self is other
      +        return self.__tree_hash__ == other.__tree_hash__
      +
      +    def __hash__(self):
      +        return self.__tree_hash__
      +
      +    @_tp_cache
      +    def __getitem__(self, params):
      +        if not isinstance(params, tuple):
      +            params = (params,)
      +        if not params and self._gorg is not Tuple:
      +            raise TypeError(
      +                "Parameter list to %s[...] cannot be empty" % _qualname(self))
      +        msg = "Parameters to generic types must be types."
      +        params = tuple(_type_check(p, msg) for p in params)
      +        if self is Generic:
      +            # Generic can only be subscripted with unique type variables.
      +            if not all(isinstance(p, TypeVar) for p in params):
      +                raise TypeError(
      +                    "Parameters to Generic[...] must all be type variables")
      +            if len(set(params)) != len(params):
      +                raise TypeError(
      +                    "Parameters to Generic[...] must all be unique")
      +            tvars = params
      +            args = params
      +        elif self in (Tuple, Callable):
      +            tvars = _type_vars(params)
      +            args = params
      +        elif self is _Protocol:
      +            # _Protocol is internal, don't check anything.
      +            tvars = params
      +            args = params
      +        elif self.__origin__ in (Generic, _Protocol):
      +            # Can't subscript Generic[...] or _Protocol[...].
      +            raise TypeError("Cannot subscript already-subscripted %s" %
      +                            repr(self))
      +        else:
      +            # Subscripting a regular Generic subclass.
      +            _check_generic(self, params)
      +            tvars = _type_vars(params)
      +            args = params
      +
      +        prepend = (self,) if self.__origin__ is None else ()
      +        return self.__class__(self.__name__,
      +                              prepend + self.__bases__,
      +                              _no_slots_copy(self.__dict__),
      +                              tvars=tvars,
      +                              args=args,
      +                              origin=self,
      +                              extra=self.__extra__,
      +                              orig_bases=self.__orig_bases__)
      +
      +    def __subclasscheck__(self, cls):
      +        if self.__origin__ is not None:
      +            if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']:
      +                raise TypeError("Parameterized generics cannot be used with class "
      +                                "or instance checks")
      +            return False
      +        if self is Generic:
      +            raise TypeError("Class %r cannot be used with class "
      +                            "or instance checks" % self)
      +        return super().__subclasscheck__(cls)
      +
      +    def __instancecheck__(self, instance):
      +        # Since we extend ABC.__subclasscheck__ and
      +        # ABC.__instancecheck__ inlines the cache checking done by the
      +        # latter, we must extend __instancecheck__ too. For simplicity
      +        # we just skip the cache check -- instance checks for generic
      +        # classes are supposed to be rare anyways.
      +        return issubclass(instance.__class__, self)
      +
      +    def __setattr__(self, attr, value):
      +        # We consider all the subscripted generics as proxies for original class
      +        if (
      +            attr.startswith('__') and attr.endswith('__') or
      +            attr.startswith('_abc_') or
      +            self._gorg is None  # The class is not fully created, see #typing/506
      +        ):
      +            super(GenericMeta, self).__setattr__(attr, value)
      +        else:
      +            super(GenericMeta, self._gorg).__setattr__(attr, value)
      +
      +
      +# Prevent checks for Generic to crash when defining Generic.
      +Generic = None
      +
      +
      +def _generic_new(base_cls, cls, *args, **kwds):
      +    # Assure type is erased on instantiation,
      +    # but attempt to store it in __orig_class__
      +    if cls.__origin__ is None:
      +        if (base_cls.__new__ is object.__new__ and
      +                cls.__init__ is not object.__init__):
      +            return base_cls.__new__(cls)
      +        else:
      +            return base_cls.__new__(cls, *args, **kwds)
      +    else:
      +        origin = cls._gorg
      +        if (base_cls.__new__ is object.__new__ and
      +                cls.__init__ is not object.__init__):
      +            obj = base_cls.__new__(origin)
      +        else:
      +            obj = base_cls.__new__(origin, *args, **kwds)
      +        try:
      +            obj.__orig_class__ = cls
      +        except AttributeError:
      +            pass
      +        obj.__init__(*args, **kwds)
      +        return obj
      +
      +
      +class Generic(metaclass=GenericMeta):
      +    """Abstract base class for generic types.
      +
      +    A generic type is typically declared by inheriting from
      +    this class parameterized with one or more type variables.
      +    For example, a generic mapping type might be defined as::
      +
      +      class Mapping(Generic[KT, VT]):
      +          def __getitem__(self, key: KT) -> VT:
      +              ...
      +          # Etc.
      +
      +    This class can then be used as follows::
      +
      +      def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
      +          try:
      +              return mapping[key]
      +          except KeyError:
      +              return default
      +    """
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is Generic:
      +            raise TypeError("Type Generic cannot be instantiated; "
      +                            "it can be used only as a base class")
      +        return _generic_new(cls.__next_in_mro__, cls, *args, **kwds)
      +
      +
      +class _TypingEmpty:
      +    """Internal placeholder for () or []. Used by TupleMeta and CallableMeta
      +    to allow empty list/tuple in specific places, without allowing them
      +    to sneak in where prohibited.
      +    """
      +
      +
      +class _TypingEllipsis:
      +    """Internal placeholder for ... (ellipsis)."""
      +
      +
      +class TupleMeta(GenericMeta):
      +    """Metaclass for Tuple (internal)."""
      +
      +    @_tp_cache
      +    def __getitem__(self, parameters):
      +        if self.__origin__ is not None or self._gorg is not Tuple:
      +            # Normal generic rules apply if this is not the first subscription
      +            # or a subscription of a subclass.
      +            return super().__getitem__(parameters)
      +        if parameters == ():
      +            return super().__getitem__((_TypingEmpty,))
      +        if not isinstance(parameters, tuple):
      +            parameters = (parameters,)
      +        if len(parameters) == 2 and parameters[1] is ...:
      +            msg = "Tuple[t, ...]: t must be a type."
      +            p = _type_check(parameters[0], msg)
      +            return super().__getitem__((p, _TypingEllipsis))
      +        msg = "Tuple[t0, t1, ...]: each t must be a type."
      +        parameters = tuple(_type_check(p, msg) for p in parameters)
      +        return super().__getitem__(parameters)
      +
      +    def __instancecheck__(self, obj):
      +        if self.__args__ is None:
      +            return isinstance(obj, tuple)
      +        raise TypeError("Parameterized Tuple cannot be used "
      +                        "with isinstance().")
      +
      +    def __subclasscheck__(self, cls):
      +        if self.__args__ is None:
      +            return issubclass(cls, tuple)
      +        raise TypeError("Parameterized Tuple cannot be used "
      +                        "with issubclass().")
      +
      +
      +class Tuple(tuple, extra=tuple, metaclass=TupleMeta):
      +    """Tuple type; Tuple[X, Y] is the cross-product type of X and Y.
      +
      +    Example: Tuple[T1, T2] is a tuple of two elements corresponding
      +    to type variables T1 and T2.  Tuple[int, float, str] is a tuple
      +    of an int, a float and a string.
      +
      +    To specify a variable-length tuple of homogeneous type, use Tuple[T, ...].
      +    """
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is Tuple:
      +            raise TypeError("Type Tuple cannot be instantiated; "
      +                            "use tuple() instead")
      +        return _generic_new(tuple, cls, *args, **kwds)
      +
      +
      +class CallableMeta(GenericMeta):
      +    """Metaclass for Callable (internal)."""
      +
      +    def __repr__(self):
      +        if self.__origin__ is None:
      +            return super().__repr__()
      +        return self._tree_repr(self._subs_tree())
      +
      +    def _tree_repr(self, tree):
      +        if self._gorg is not Callable:
      +            return super()._tree_repr(tree)
      +        # For actual Callable (not its subclass) we override
      +        # super()._tree_repr() for nice formatting.
      +        arg_list = []
      +        for arg in tree[1:]:
      +            if not isinstance(arg, tuple):
      +                arg_list.append(_type_repr(arg))
      +            else:
      +                arg_list.append(arg[0]._tree_repr(arg))
      +        if arg_list[0] == '...':
      +            return repr(tree[0]) + '[..., %s]' % arg_list[1]
      +        return (repr(tree[0]) +
      +                '[[%s], %s]' % (', '.join(arg_list[:-1]), arg_list[-1]))
      +
      +    def __getitem__(self, parameters):
      +        """A thin wrapper around __getitem_inner__ to provide the latter
      +        with hashable arguments to improve speed.
      +        """
      +
      +        if self.__origin__ is not None or self._gorg is not Callable:
      +            return super().__getitem__(parameters)
      +        if not isinstance(parameters, tuple) or len(parameters) != 2:
      +            raise TypeError("Callable must be used as "
      +                            "Callable[[arg, ...], result].")
      +        args, result = parameters
      +        if args is Ellipsis:
      +            parameters = (Ellipsis, result)
      +        else:
      +            if not isinstance(args, list):
      +                raise TypeError("Callable[args, result]: args must be a list."
      +                                " Got %.100r." % (args,))
      +            parameters = (tuple(args), result)
      +        return self.__getitem_inner__(parameters)
      +
      +    @_tp_cache
      +    def __getitem_inner__(self, parameters):
      +        args, result = parameters
      +        msg = "Callable[args, result]: result must be a type."
      +        result = _type_check(result, msg)
      +        if args is Ellipsis:
      +            return super().__getitem__((_TypingEllipsis, result))
      +        msg = "Callable[[arg, ...], result]: each arg must be a type."
      +        args = tuple(_type_check(arg, msg) for arg in args)
      +        parameters = args + (result,)
      +        return super().__getitem__(parameters)
      +
      +
      +class Callable(extra=collections_abc.Callable, metaclass=CallableMeta):
      +    """Callable type; Callable[[int], str] is a function of (int) -> str.
      +
      +    The subscription syntax must always be used with exactly two
      +    values: the argument list and the return type.  The argument list
      +    must be a list of types or ellipsis; the return type must be a single type.
      +
      +    There is no syntax to indicate optional or keyword arguments,
      +    such function types are rarely used as callback types.
      +    """
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is Callable:
      +            raise TypeError("Type Callable cannot be instantiated; "
      +                            "use a non-abstract subclass instead")
      +        return _generic_new(cls.__next_in_mro__, cls, *args, **kwds)
      +
      +
      +class _ClassVar(_FinalTypingBase, _root=True):
      +    """Special type construct to mark class variables.
      +
      +    An annotation wrapped in ClassVar indicates that a given
      +    attribute is intended to be used as a class variable and
      +    should not be set on instances of that class. Usage::
      +
      +      class Starship:
      +          stats: ClassVar[Dict[str, int]] = {} # class variable
      +          damage: int = 10                     # instance variable
      +
      +    ClassVar accepts only types and cannot be further subscribed.
      +
      +    Note that ClassVar is not a class itself, and should not
      +    be used with isinstance() or issubclass().
      +    """
      +
      +    __slots__ = ('__type__',)
      +
      +    def __init__(self, tp=None, **kwds):
      +        self.__type__ = tp
      +
      +    def __getitem__(self, item):
      +        cls = type(self)
      +        if self.__type__ is None:
      +            return cls(_type_check(item,
      +                       '{} accepts only single type.'.format(cls.__name__[1:])),
      +                       _root=True)
      +        raise TypeError('{} cannot be further subscripted'
      +                        .format(cls.__name__[1:]))
      +
      +    def _eval_type(self, globalns, localns):
      +        new_tp = _eval_type(self.__type__, globalns, localns)
      +        if new_tp == self.__type__:
      +            return self
      +        return type(self)(new_tp, _root=True)
      +
      +    def __repr__(self):
      +        r = super().__repr__()
      +        if self.__type__ is not None:
      +            r += '[{}]'.format(_type_repr(self.__type__))
      +        return r
      +
      +    def __hash__(self):
      +        return hash((type(self).__name__, self.__type__))
      +
      +    def __eq__(self, other):
      +        if not isinstance(other, _ClassVar):
      +            return NotImplemented
      +        if self.__type__ is not None:
      +            return self.__type__ == other.__type__
      +        return self is other
      +
      +
      +ClassVar = _ClassVar(_root=True)
      +
      +
      +def cast(typ, val):
      +    """Cast a value to a type.
      +
      +    This returns the value unchanged.  To the type checker this
      +    signals that the return value has the designated type, but at
      +    runtime we intentionally don't check anything (we want this
      +    to be as fast as possible).
      +    """
      +    return val
      +
      +
      +def _get_defaults(func):
      +    """Internal helper to extract the default arguments, by name."""
      +    try:
      +        code = func.__code__
      +    except AttributeError:
      +        # Some built-in functions don't have __code__, __defaults__, etc.
      +        return {}
      +    pos_count = code.co_argcount
      +    arg_names = code.co_varnames
      +    arg_names = arg_names[:pos_count]
      +    defaults = func.__defaults__ or ()
      +    kwdefaults = func.__kwdefaults__
      +    res = dict(kwdefaults) if kwdefaults else {}
      +    pos_offset = pos_count - len(defaults)
      +    for name, value in zip(arg_names[pos_offset:], defaults):
      +        assert name not in res
      +        res[name] = value
      +    return res
      +
      +
      +_allowed_types = (types.FunctionType, types.BuiltinFunctionType,
      +                  types.MethodType, types.ModuleType,
      +                  WrapperDescriptorType, MethodWrapperType, MethodDescriptorType)
      +
      +
      +def get_type_hints(obj, globalns=None, localns=None):
      +    """Return type hints for an object.
      +
      +    This is often the same as obj.__annotations__, but it handles
      +    forward references encoded as string literals, and if necessary
      +    adds Optional[t] if a default value equal to None is set.
      +
      +    The argument may be a module, class, method, or function. The annotations
      +    are returned as a dictionary. For classes, annotations include also
      +    inherited members.
      +
      +    TypeError is raised if the argument is not of a type that can contain
      +    annotations, and an empty dictionary is returned if no annotations are
      +    present.
      +
      +    BEWARE -- the behavior of globalns and localns is counterintuitive
      +    (unless you are familiar with how eval() and exec() work).  The
      +    search order is locals first, then globals.
      +
      +    - If no dict arguments are passed, an attempt is made to use the
      +      globals from obj (or the respective module's globals for classes),
      +      and these are also used as the locals.  If the object does not appear
      +      to have globals, an empty dictionary is used.
      +
      +    - If one dict argument is passed, it is used for both globals and
      +      locals.
      +
      +    - If two dict arguments are passed, they specify globals and
      +      locals, respectively.
      +    """
      +
      +    if getattr(obj, '__no_type_check__', None):
      +        return {}
      +    # Classes require a special treatment.
      +    if isinstance(obj, type):
      +        hints = {}
      +        for base in reversed(obj.__mro__):
      +            if globalns is None:
      +                base_globals = sys.modules[base.__module__].__dict__
      +            else:
      +                base_globals = globalns
      +            ann = base.__dict__.get('__annotations__', {})
      +            for name, value in ann.items():
      +                if value is None:
      +                    value = type(None)
      +                if isinstance(value, str):
      +                    value = _ForwardRef(value)
      +                value = _eval_type(value, base_globals, localns)
      +                hints[name] = value
      +        return hints
      +
      +    if globalns is None:
      +        if isinstance(obj, types.ModuleType):
      +            globalns = obj.__dict__
      +        else:
      +            globalns = getattr(obj, '__globals__', {})
      +        if localns is None:
      +            localns = globalns
      +    elif localns is None:
      +        localns = globalns
      +    hints = getattr(obj, '__annotations__', None)
      +    if hints is None:
      +        # Return empty annotations for something that _could_ have them.
      +        if isinstance(obj, _allowed_types):
      +            return {}
      +        else:
      +            raise TypeError('{!r} is not a module, class, method, '
      +                            'or function.'.format(obj))
      +    defaults = _get_defaults(obj)
      +    hints = dict(hints)
      +    for name, value in hints.items():
      +        if value is None:
      +            value = type(None)
      +        if isinstance(value, str):
      +            value = _ForwardRef(value)
      +        value = _eval_type(value, globalns, localns)
      +        if name in defaults and defaults[name] is None:
      +            value = Optional[value]
      +        hints[name] = value
      +    return hints
      +
      +
      +def no_type_check(arg):
      +    """Decorator to indicate that annotations are not type hints.
      +
      +    The argument must be a class or function; if it is a class, it
      +    applies recursively to all methods and classes defined in that class
      +    (but not to methods defined in its superclasses or subclasses).
      +
      +    This mutates the function(s) or class(es) in place.
      +    """
      +    if isinstance(arg, type):
      +        arg_attrs = arg.__dict__.copy()
      +        for attr, val in arg.__dict__.items():
      +            if val in arg.__bases__ + (arg,):
      +                arg_attrs.pop(attr)
      +        for obj in arg_attrs.values():
      +            if isinstance(obj, types.FunctionType):
      +                obj.__no_type_check__ = True
      +            if isinstance(obj, type):
      +                no_type_check(obj)
      +    try:
      +        arg.__no_type_check__ = True
      +    except TypeError:  # built-in classes
      +        pass
      +    return arg
      +
      +
      +def no_type_check_decorator(decorator):
      +    """Decorator to give another decorator the @no_type_check effect.
      +
      +    This wraps the decorator with something that wraps the decorated
      +    function in @no_type_check.
      +    """
      +
      +    @functools.wraps(decorator)
      +    def wrapped_decorator(*args, **kwds):
      +        func = decorator(*args, **kwds)
      +        func = no_type_check(func)
      +        return func
      +
      +    return wrapped_decorator
      +
      +
      +def _overload_dummy(*args, **kwds):
      +    """Helper for @overload to raise when called."""
      +    raise NotImplementedError(
      +        "You should not call an overloaded function. "
      +        "A series of @overload-decorated functions "
      +        "outside a stub module should always be followed "
      +        "by an implementation that is not @overload-ed.")
      +
      +
      +def overload(func):
      +    """Decorator for overloaded functions/methods.
      +
      +    In a stub file, place two or more stub definitions for the same
      +    function in a row, each decorated with @overload.  For example:
      +
      +      @overload
      +      def utf8(value: None) -> None: ...
      +      @overload
      +      def utf8(value: bytes) -> bytes: ...
      +      @overload
      +      def utf8(value: str) -> bytes: ...
      +
      +    In a non-stub file (i.e. a regular .py file), do the same but
      +    follow it with an implementation.  The implementation should *not*
      +    be decorated with @overload.  For example:
      +
      +      @overload
      +      def utf8(value: None) -> None: ...
      +      @overload
      +      def utf8(value: bytes) -> bytes: ...
      +      @overload
      +      def utf8(value: str) -> bytes: ...
      +      def utf8(value):
      +          # implementation goes here
      +    """
      +    return _overload_dummy
      +
      +
      +class _ProtocolMeta(GenericMeta):
      +    """Internal metaclass for _Protocol.
      +
      +    This exists so _Protocol classes can be generic without deriving
      +    from Generic.
      +    """
      +
      +    def __instancecheck__(self, obj):
      +        if _Protocol not in self.__bases__:
      +            return super().__instancecheck__(obj)
      +        raise TypeError("Protocols cannot be used with isinstance().")
      +
      +    def __subclasscheck__(self, cls):
      +        if not self._is_protocol:
      +            # No structural checks since this isn't a protocol.
      +            return NotImplemented
      +
      +        if self is _Protocol:
      +            # Every class is a subclass of the empty protocol.
      +            return True
      +
      +        # Find all attributes defined in the protocol.
      +        attrs = self._get_protocol_attrs()
      +
      +        for attr in attrs:
      +            if not any(attr in d.__dict__ for d in cls.__mro__):
      +                return False
      +        return True
      +
      +    def _get_protocol_attrs(self):
      +        # Get all Protocol base classes.
      +        protocol_bases = []
      +        for c in self.__mro__:
      +            if getattr(c, '_is_protocol', False) and c.__name__ != '_Protocol':
      +                protocol_bases.append(c)
      +
      +        # Get attributes included in protocol.
      +        attrs = set()
      +        for base in protocol_bases:
      +            for attr in base.__dict__.keys():
      +                # Include attributes not defined in any non-protocol bases.
      +                for c in self.__mro__:
      +                    if (c is not base and attr in c.__dict__ and
      +                            not getattr(c, '_is_protocol', False)):
      +                        break
      +                else:
      +                    if (not attr.startswith('_abc_') and
      +                            attr != '__abstractmethods__' and
      +                            attr != '__annotations__' and
      +                            attr != '__weakref__' and
      +                            attr != '_is_protocol' and
      +                            attr != '_gorg' and
      +                            attr != '__dict__' and
      +                            attr != '__args__' and
      +                            attr != '__slots__' and
      +                            attr != '_get_protocol_attrs' and
      +                            attr != '__next_in_mro__' and
      +                            attr != '__parameters__' and
      +                            attr != '__origin__' and
      +                            attr != '__orig_bases__' and
      +                            attr != '__extra__' and
      +                            attr != '__tree_hash__' and
      +                            attr != '__module__'):
      +                        attrs.add(attr)
      +
      +        return attrs
      +
      +
      +class _Protocol(metaclass=_ProtocolMeta):
      +    """Internal base class for protocol classes.
      +
      +    This implements a simple-minded structural issubclass check
      +    (similar but more general than the one-offs in collections.abc
      +    such as Hashable).
      +    """
      +
      +    __slots__ = ()
      +
      +    _is_protocol = True
      +
      +
      +# Various ABCs mimicking those in collections.abc.
      +# A few are simply re-exported for completeness.
      +
      +Hashable = collections_abc.Hashable  # Not generic.
      +
      +
      +if hasattr(collections_abc, 'Awaitable'):
      +    class Awaitable(Generic[T_co], extra=collections_abc.Awaitable):
      +        __slots__ = ()
      +
      +    __all__.append('Awaitable')
      +
      +
      +if hasattr(collections_abc, 'Coroutine'):
      +    class Coroutine(Awaitable[V_co], Generic[T_co, T_contra, V_co],
      +                    extra=collections_abc.Coroutine):
      +        __slots__ = ()
      +
      +    __all__.append('Coroutine')
      +
      +
      +if hasattr(collections_abc, 'AsyncIterable'):
      +
      +    class AsyncIterable(Generic[T_co], extra=collections_abc.AsyncIterable):
      +        __slots__ = ()
      +
      +    class AsyncIterator(AsyncIterable[T_co],
      +                        extra=collections_abc.AsyncIterator):
      +        __slots__ = ()
      +
      +    __all__.append('AsyncIterable')
      +    __all__.append('AsyncIterator')
      +
      +
      +class Iterable(Generic[T_co], extra=collections_abc.Iterable):
      +    __slots__ = ()
      +
      +
      +class Iterator(Iterable[T_co], extra=collections_abc.Iterator):
      +    __slots__ = ()
      +
      +
      +class SupportsInt(_Protocol):
      +    __slots__ = ()
      +
      +    @abstractmethod
      +    def __int__(self) -> int:
      +        pass
      +
      +
      +class SupportsFloat(_Protocol):
      +    __slots__ = ()
      +
      +    @abstractmethod
      +    def __float__(self) -> float:
      +        pass
      +
      +
      +class SupportsComplex(_Protocol):
      +    __slots__ = ()
      +
      +    @abstractmethod
      +    def __complex__(self) -> complex:
      +        pass
      +
      +
      +class SupportsBytes(_Protocol):
      +    __slots__ = ()
      +
      +    @abstractmethod
      +    def __bytes__(self) -> bytes:
      +        pass
      +
      +
      +class SupportsAbs(_Protocol[T_co]):
      +    __slots__ = ()
      +
      +    @abstractmethod
      +    def __abs__(self) -> T_co:
      +        pass
      +
      +
      +class SupportsRound(_Protocol[T_co]):
      +    __slots__ = ()
      +
      +    @abstractmethod
      +    def __round__(self, ndigits: int = 0) -> T_co:
      +        pass
      +
      +
      +if hasattr(collections_abc, 'Reversible'):
      +    class Reversible(Iterable[T_co], extra=collections_abc.Reversible):
      +        __slots__ = ()
      +else:
      +    class Reversible(_Protocol[T_co]):
      +        __slots__ = ()
      +
      +        @abstractmethod
      +        def __reversed__(self) -> 'Iterator[T_co]':
      +            pass
      +
      +
      +Sized = collections_abc.Sized  # Not generic.
      +
      +
      +class Container(Generic[T_co], extra=collections_abc.Container):
      +    __slots__ = ()
      +
      +
      +if hasattr(collections_abc, 'Collection'):
      +    class Collection(Sized, Iterable[T_co], Container[T_co],
      +                     extra=collections_abc.Collection):
      +        __slots__ = ()
      +
      +    __all__.append('Collection')
      +
      +
      +# Callable was defined earlier.
      +
      +if hasattr(collections_abc, 'Collection'):
      +    class AbstractSet(Collection[T_co],
      +                      extra=collections_abc.Set):
      +        __slots__ = ()
      +else:
      +    class AbstractSet(Sized, Iterable[T_co], Container[T_co],
      +                      extra=collections_abc.Set):
      +        __slots__ = ()
      +
      +
      +class MutableSet(AbstractSet[T], extra=collections_abc.MutableSet):
      +    __slots__ = ()
      +
      +
      +# NOTE: It is only covariant in the value type.
      +if hasattr(collections_abc, 'Collection'):
      +    class Mapping(Collection[KT], Generic[KT, VT_co],
      +                  extra=collections_abc.Mapping):
      +        __slots__ = ()
      +else:
      +    class Mapping(Sized, Iterable[KT], Container[KT], Generic[KT, VT_co],
      +                  extra=collections_abc.Mapping):
      +        __slots__ = ()
      +
      +
      +class MutableMapping(Mapping[KT, VT], extra=collections_abc.MutableMapping):
      +    __slots__ = ()
      +
      +
      +if hasattr(collections_abc, 'Reversible'):
      +    if hasattr(collections_abc, 'Collection'):
      +        class Sequence(Reversible[T_co], Collection[T_co],
      +                       extra=collections_abc.Sequence):
      +            __slots__ = ()
      +    else:
      +        class Sequence(Sized, Reversible[T_co], Container[T_co],
      +                       extra=collections_abc.Sequence):
      +            __slots__ = ()
      +else:
      +    class Sequence(Sized, Iterable[T_co], Container[T_co],
      +                   extra=collections_abc.Sequence):
      +        __slots__ = ()
      +
      +
      +class MutableSequence(Sequence[T], extra=collections_abc.MutableSequence):
      +    __slots__ = ()
      +
      +
      +class ByteString(Sequence[int], extra=collections_abc.ByteString):
      +    __slots__ = ()
      +
      +
      +class List(list, MutableSequence[T], extra=list):
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is List:
      +            raise TypeError("Type List cannot be instantiated; "
      +                            "use list() instead")
      +        return _generic_new(list, cls, *args, **kwds)
      +
      +
      +class Deque(collections.deque, MutableSequence[T], extra=collections.deque):
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is Deque:
      +            return collections.deque(*args, **kwds)
      +        return _generic_new(collections.deque, cls, *args, **kwds)
      +
      +
      +class Set(set, MutableSet[T], extra=set):
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is Set:
      +            raise TypeError("Type Set cannot be instantiated; "
      +                            "use set() instead")
      +        return _generic_new(set, cls, *args, **kwds)
      +
      +
      +class FrozenSet(frozenset, AbstractSet[T_co], extra=frozenset):
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is FrozenSet:
      +            raise TypeError("Type FrozenSet cannot be instantiated; "
      +                            "use frozenset() instead")
      +        return _generic_new(frozenset, cls, *args, **kwds)
      +
      +
      +class MappingView(Sized, Iterable[T_co], extra=collections_abc.MappingView):
      +    __slots__ = ()
      +
      +
      +class KeysView(MappingView[KT], AbstractSet[KT],
      +               extra=collections_abc.KeysView):
      +    __slots__ = ()
      +
      +
      +class ItemsView(MappingView[Tuple[KT, VT_co]],
      +                AbstractSet[Tuple[KT, VT_co]],
      +                Generic[KT, VT_co],
      +                extra=collections_abc.ItemsView):
      +    __slots__ = ()
      +
      +
      +class ValuesView(MappingView[VT_co], extra=collections_abc.ValuesView):
      +    __slots__ = ()
      +
      +
      +if hasattr(contextlib, 'AbstractContextManager'):
      +    class ContextManager(Generic[T_co], extra=contextlib.AbstractContextManager):
      +        __slots__ = ()
      +else:
      +    class ContextManager(Generic[T_co]):
      +        __slots__ = ()
      +
      +        def __enter__(self):
      +            return self
      +
      +        @abc.abstractmethod
      +        def __exit__(self, exc_type, exc_value, traceback):
      +            return None
      +
      +        @classmethod
      +        def __subclasshook__(cls, C):
      +            if cls is ContextManager:
      +                # In Python 3.6+, it is possible to set a method to None to
      +                # explicitly indicate that the class does not implement an ABC
      +                # (https://bugs.python.org/issue25958), but we do not support
      +                # that pattern here because this fallback class is only used
      +                # in Python 3.5 and earlier.
      +                if (any("__enter__" in B.__dict__ for B in C.__mro__) and
      +                    any("__exit__" in B.__dict__ for B in C.__mro__)):
      +                    return True
      +            return NotImplemented
      +
      +
      +if hasattr(contextlib, 'AbstractAsyncContextManager'):
      +    class AsyncContextManager(Generic[T_co],
      +                              extra=contextlib.AbstractAsyncContextManager):
      +        __slots__ = ()
      +
      +    __all__.append('AsyncContextManager')
      +elif sys.version_info[:2] >= (3, 5):
      +    exec("""
      +class AsyncContextManager(Generic[T_co]):
      +    __slots__ = ()
      +
      +    async def __aenter__(self):
      +        return self
      +
      +    @abc.abstractmethod
      +    async def __aexit__(self, exc_type, exc_value, traceback):
      +        return None
      +
      +    @classmethod
      +    def __subclasshook__(cls, C):
      +        if cls is AsyncContextManager:
      +            if sys.version_info[:2] >= (3, 6):
      +                return _collections_abc._check_methods(C, "__aenter__", "__aexit__")
      +            if (any("__aenter__" in B.__dict__ for B in C.__mro__) and
      +                    any("__aexit__" in B.__dict__ for B in C.__mro__)):
      +                return True
      +        return NotImplemented
      +
      +__all__.append('AsyncContextManager')
      +""")
      +
      +
      +class Dict(dict, MutableMapping[KT, VT], extra=dict):
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is Dict:
      +            raise TypeError("Type Dict cannot be instantiated; "
      +                            "use dict() instead")
      +        return _generic_new(dict, cls, *args, **kwds)
      +
      +
      +class DefaultDict(collections.defaultdict, MutableMapping[KT, VT],
      +                  extra=collections.defaultdict):
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is DefaultDict:
      +            return collections.defaultdict(*args, **kwds)
      +        return _generic_new(collections.defaultdict, cls, *args, **kwds)
      +
      +
      +class Counter(collections.Counter, Dict[T, int], extra=collections.Counter):
      +
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is Counter:
      +            return collections.Counter(*args, **kwds)
      +        return _generic_new(collections.Counter, cls, *args, **kwds)
      +
      +
      +if hasattr(collections, 'ChainMap'):
      +    # ChainMap only exists in 3.3+
      +    __all__.append('ChainMap')
      +
      +    class ChainMap(collections.ChainMap, MutableMapping[KT, VT],
      +                   extra=collections.ChainMap):
      +
      +        __slots__ = ()
      +
      +        def __new__(cls, *args, **kwds):
      +            if cls._gorg is ChainMap:
      +                return collections.ChainMap(*args, **kwds)
      +            return _generic_new(collections.ChainMap, cls, *args, **kwds)
      +
      +
      +# Determine what base class to use for Generator.
      +if hasattr(collections_abc, 'Generator'):
      +    # Sufficiently recent versions of 3.5 have a Generator ABC.
      +    _G_base = collections_abc.Generator
      +else:
      +    # Fall back on the exact type.
      +    _G_base = types.GeneratorType
      +
      +
      +class Generator(Iterator[T_co], Generic[T_co, T_contra, V_co],
      +                extra=_G_base):
      +    __slots__ = ()
      +
      +    def __new__(cls, *args, **kwds):
      +        if cls._gorg is Generator:
      +            raise TypeError("Type Generator cannot be instantiated; "
      +                            "create a subclass instead")
      +        return _generic_new(_G_base, cls, *args, **kwds)
      +
      +
      +if hasattr(collections_abc, 'AsyncGenerator'):
      +    class AsyncGenerator(AsyncIterator[T_co], Generic[T_co, T_contra],
      +                         extra=collections_abc.AsyncGenerator):
      +        __slots__ = ()
      +
      +    __all__.append('AsyncGenerator')
      +
      +
      +# Internal type variable used for Type[].
      +CT_co = TypeVar('CT_co', covariant=True, bound=type)
      +
      +
      +# This is not a real generic class.  Don't use outside annotations.
      +class Type(Generic[CT_co], extra=type):
      +    """A special construct usable to annotate class objects.
      +
      +    For example, suppose we have the following classes::
      +
      +      class User: ...  # Abstract base for User classes
      +      class BasicUser(User): ...
      +      class ProUser(User): ...
      +      class TeamUser(User): ...
      +
      +    And a function that takes a class argument that's a subclass of
      +    User and returns an instance of the corresponding class::
      +
      +      U = TypeVar('U', bound=User)
      +      def new_user(user_class: Type[U]) -> U:
      +          user = user_class()
      +          # (Here we could write the user object to a database)
      +          return user
      +
      +      joe = new_user(BasicUser)
      +
      +    At this point the type checker knows that joe has type BasicUser.
      +    """
      +
      +    __slots__ = ()
      +
      +
      +def _make_nmtuple(name, types):
      +    msg = "NamedTuple('Name', [(f0, t0), (f1, t1), ...]); each t must be a type"
      +    types = [(n, _type_check(t, msg)) for n, t in types]
      +    nm_tpl = collections.namedtuple(name, [n for n, t in types])
      +    # Prior to PEP 526, only _field_types attribute was assigned.
      +    # Now, both __annotations__ and _field_types are used to maintain compatibility.
      +    nm_tpl.__annotations__ = nm_tpl._field_types = collections.OrderedDict(types)
      +    try:
      +        nm_tpl.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__')
      +    except (AttributeError, ValueError):
      +        pass
      +    return nm_tpl
      +
      +
      +_PY36 = sys.version_info[:2] >= (3, 6)
      +
      +# attributes prohibited to set in NamedTuple class syntax
      +_prohibited = ('__new__', '__init__', '__slots__', '__getnewargs__',
      +               '_fields', '_field_defaults', '_field_types',
      +               '_make', '_replace', '_asdict', '_source')
      +
      +_special = ('__module__', '__name__', '__qualname__', '__annotations__')
      +
      +
      +class NamedTupleMeta(type):
      +
      +    def __new__(cls, typename, bases, ns):
      +        if ns.get('_root', False):
      +            return super().__new__(cls, typename, bases, ns)
      +        if not _PY36:
      +            raise TypeError("Class syntax for NamedTuple is only supported"
      +                            " in Python 3.6+")
      +        types = ns.get('__annotations__', {})
      +        nm_tpl = _make_nmtuple(typename, types.items())
      +        defaults = []
      +        defaults_dict = {}
      +        for field_name in types:
      +            if field_name in ns:
      +                default_value = ns[field_name]
      +                defaults.append(default_value)
      +                defaults_dict[field_name] = default_value
      +            elif defaults:
      +                raise TypeError("Non-default namedtuple field {field_name} cannot "
      +                                "follow default field(s) {default_names}"
      +                                .format(field_name=field_name,
      +                                        default_names=', '.join(defaults_dict.keys())))
      +        nm_tpl.__new__.__annotations__ = collections.OrderedDict(types)
      +        nm_tpl.__new__.__defaults__ = tuple(defaults)
      +        nm_tpl._field_defaults = defaults_dict
      +        # update from user namespace without overriding special namedtuple attributes
      +        for key in ns:
      +            if key in _prohibited:
      +                raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
      +            elif key not in _special and key not in nm_tpl._fields:
      +                setattr(nm_tpl, key, ns[key])
      +        return nm_tpl
      +
      +
      +class NamedTuple(metaclass=NamedTupleMeta):
      +    """Typed version of namedtuple.
      +
      +    Usage in Python versions >= 3.6::
      +
      +        class Employee(NamedTuple):
      +            name: str
      +            id: int
      +
      +    This is equivalent to::
      +
      +        Employee = collections.namedtuple('Employee', ['name', 'id'])
      +
      +    The resulting class has extra __annotations__ and _field_types
      +    attributes, giving an ordered dict mapping field names to types.
      +    __annotations__ should be preferred, while _field_types
      +    is kept to maintain pre PEP 526 compatibility. (The field names
      +    are in the _fields attribute, which is part of the namedtuple
      +    API.) Alternative equivalent keyword syntax is also accepted::
      +
      +        Employee = NamedTuple('Employee', name=str, id=int)
      +
      +    In Python versions <= 3.5 use::
      +
      +        Employee = NamedTuple('Employee', [('name', str), ('id', int)])
      +    """
      +    _root = True
      +
      +    def __new__(self, typename, fields=None, **kwargs):
      +        if kwargs and not _PY36:
      +            raise TypeError("Keyword syntax for NamedTuple is only supported"
      +                            " in Python 3.6+")
      +        if fields is None:
      +            fields = kwargs.items()
      +        elif kwargs:
      +            raise TypeError("Either list of fields or keywords"
      +                            " can be provided to NamedTuple, not both")
      +        return _make_nmtuple(typename, fields)
      +
      +
      +def NewType(name, tp):
      +    """NewType creates simple unique types with almost zero
      +    runtime overhead. NewType(name, tp) is considered a subtype of tp
      +    by static type checkers. At runtime, NewType(name, tp) returns
      +    a dummy function that simply returns its argument. Usage::
      +
      +        UserId = NewType('UserId', int)
      +
      +        def name_by_id(user_id: UserId) -> str:
      +            ...
      +
      +        UserId('user')          # Fails type check
      +
      +        name_by_id(42)          # Fails type check
      +        name_by_id(UserId(42))  # OK
      +
      +        num = UserId(5) + 1     # type: int
      +    """
      +
      +    def new_type(x):
      +        return x
      +
      +    new_type.__name__ = name
      +    new_type.__supertype__ = tp
      +    return new_type
      +
      +
      +# Python-version-specific alias (Python 2: unicode; Python 3: str)
      +Text = str
      +
      +
      +# Constant that's True when type checking, but False here.
      +TYPE_CHECKING = False
      +
      +
      +class IO(Generic[AnyStr]):
      +    """Generic base class for TextIO and BinaryIO.
      +
      +    This is an abstract, generic version of the return of open().
      +
      +    NOTE: This does not distinguish between the different possible
      +    classes (text vs. binary, read vs. write vs. read/write,
      +    append-only, unbuffered).  The TextIO and BinaryIO subclasses
      +    below capture the distinctions between text vs. binary, which is
      +    pervasive in the interface; however we currently do not offer a
      +    way to track the other distinctions in the type system.
      +    """
      +
      +    __slots__ = ()
      +
      +    @abstractproperty
      +    def mode(self) -> str:
      +        pass
      +
      +    @abstractproperty
      +    def name(self) -> str:
      +        pass
      +
      +    @abstractmethod
      +    def close(self) -> None:
      +        pass
      +
      +    @abstractmethod
      +    def closed(self) -> bool:
      +        pass
      +
      +    @abstractmethod
      +    def fileno(self) -> int:
      +        pass
      +
      +    @abstractmethod
      +    def flush(self) -> None:
      +        pass
      +
      +    @abstractmethod
      +    def isatty(self) -> bool:
      +        pass
      +
      +    @abstractmethod
      +    def read(self, n: int = -1) -> AnyStr:
      +        pass
      +
      +    @abstractmethod
      +    def readable(self) -> bool:
      +        pass
      +
      +    @abstractmethod
      +    def readline(self, limit: int = -1) -> AnyStr:
      +        pass
      +
      +    @abstractmethod
      +    def readlines(self, hint: int = -1) -> List[AnyStr]:
      +        pass
      +
      +    @abstractmethod
      +    def seek(self, offset: int, whence: int = 0) -> int:
      +        pass
      +
      +    @abstractmethod
      +    def seekable(self) -> bool:
      +        pass
      +
      +    @abstractmethod
      +    def tell(self) -> int:
      +        pass
      +
      +    @abstractmethod
      +    def truncate(self, size: int = None) -> int:
      +        pass
      +
      +    @abstractmethod
      +    def writable(self) -> bool:
      +        pass
      +
      +    @abstractmethod
      +    def write(self, s: AnyStr) -> int:
      +        pass
      +
      +    @abstractmethod
      +    def writelines(self, lines: List[AnyStr]) -> None:
      +        pass
      +
      +    @abstractmethod
      +    def __enter__(self) -> 'IO[AnyStr]':
      +        pass
      +
      +    @abstractmethod
      +    def __exit__(self, type, value, traceback) -> None:
      +        pass
      +
      +
      +class BinaryIO(IO[bytes]):
      +    """Typed version of the return of open() in binary mode."""
      +
      +    __slots__ = ()
      +
      +    @abstractmethod
      +    def write(self, s: Union[bytes, bytearray]) -> int:
      +        pass
      +
      +    @abstractmethod
      +    def __enter__(self) -> 'BinaryIO':
      +        pass
      +
      +
      +class TextIO(IO[str]):
      +    """Typed version of the return of open() in text mode."""
      +
      +    __slots__ = ()
      +
      +    @abstractproperty
      +    def buffer(self) -> BinaryIO:
      +        pass
      +
      +    @abstractproperty
      +    def encoding(self) -> str:
      +        pass
      +
      +    @abstractproperty
      +    def errors(self) -> Optional[str]:
      +        pass
      +
      +    @abstractproperty
      +    def line_buffering(self) -> bool:
      +        pass
      +
      +    @abstractproperty
      +    def newlines(self) -> Any:
      +        pass
      +
      +    @abstractmethod
      +    def __enter__(self) -> 'TextIO':
      +        pass
      +
      +
      +class io:
      +    """Wrapper namespace for IO generic classes."""
      +
      +    __all__ = ['IO', 'TextIO', 'BinaryIO']
      +    IO = IO
      +    TextIO = TextIO
      +    BinaryIO = BinaryIO
      +
      +
      +io.__name__ = __name__ + '.io'
      +sys.modules[io.__name__] = io
      +
      +
      +Pattern = _TypeAlias('Pattern', AnyStr, type(stdlib_re.compile('')),
      +                     lambda p: p.pattern)
      +Match = _TypeAlias('Match', AnyStr, type(stdlib_re.match('', '')),
      +                   lambda m: m.re.pattern)
      +
      +
      +class re:
      +    """Wrapper namespace for re type aliases."""
      +
      +    __all__ = ['Pattern', 'Match']
      +    Pattern = Pattern
      +    Match = Match
      +
      +
      +re.__name__ = __name__ + '.re'
      +sys.modules[re.__name__] = re
      diff --git a/src/image_occlusion_enhanced/libaddon/_version.py b/src/image_occlusion_enhanced/libaddon/_version.py
      new file mode 100644
      index 00000000..af0cd4f6
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/_version.py
      @@ -0,0 +1,36 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Version information
      +"""
      +
      +__version__ = "0.1.0-dev.0"
      diff --git a/src/image_occlusion_enhanced/libaddon/anki/__init__.py b/src/image_occlusion_enhanced/libaddon/anki/__init__.py
      new file mode 100644
      index 00000000..4082140f
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/anki/__init__.py
      @@ -0,0 +1,35 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Package that bundles together commonly used modules that mediate interaction
      +between add-ons and Anki
      +"""
      diff --git a/src/image_occlusion_enhanced/libaddon/anki/configeditor.py b/src/image_occlusion_enhanced/libaddon/anki/configeditor.py
      new file mode 100644
      index 00000000..cce3269f
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/anki/configeditor.py
      @@ -0,0 +1,103 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +# Copyright (C) 2016-2019  Ankitects Pty Ltd and contributors
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +anki20 compat: Add-on configuration editor
      +"""
      +
      +import aqt
      +from aqt.qt import *
      +from aqt.utils import tooltip
      +
      +from anki.utils import json
      +
      +from ..consts import ADDON
      +from .._vendor import markdown2
      +from ..platform import PATH_THIS_ADDON
      +
      +from .dialog_htmlview import HTMLViewer
      +
      +class ConfigEditor(QDialog):
      +    
      +    def __init__(self, config_manager, parent):
      +        super(ConfigEditor, self).__init__(parent=parent)
      +        self.mgr = config_manager
      +        self.form = aqt.forms.editaddon.Ui_Dialog()
      +        self.form.setupUi(self)
      +        self.setWindowTitle("{} Configuration".format(ADDON.NAME))
      +        self.setupWidgets()
      +        self.updateText(self.mgr["local"])
      +        self.exec_()
      +    
      +    def setupWidgets(self):
      +        button_box = self.form.buttonBox
      +        restore_btn = button_box.addButton(QDialogButtonBox.RestoreDefaults)
      +        help_btn = button_box.addButton(QDialogButtonBox.Help)
      +        help_btn.clicked.connect(self.onHelpRequested)
      +        restore_btn.clicked.connect(self.onRestoreDefaults)
      +    
      +    def updateText(self, conf):
      +        self.form.text.setPlainText(
      +            json.dumps(conf, ensure_ascii=False, sort_keys=True,
      +                       indent=4, separators=(',', ': ')))
      +    
      +    def onRestoreDefaults(self):
      +        default_conf = self.mgr.defaults["local"]
      +        self.updateText(default_conf)
      +        tooltip("Restored defaults", parent=self)
      +        
      +    def onHelpRequested(self):
      +        docs_path = os.path.join(PATH_THIS_ADDON, "config.md")
      +        if not os.path.exists(docs_path):
      +            return False
      +        with open(docs_path, "r") as f:
      +            html = markdown2.markdown(f.read())
      +        dialog = HTMLViewer(html, title="{} Configuration Help".format(
      +            ADDON.NAME), parent=self)
      +        dialog.show()
      +    
      +    def accept(self):
      +        txt = self.form.text.toPlainText()
      +        try:
      +            new_conf = json.loads(txt)
      +        except ValueError as e:
      +            showInfo("Invalid configuration, restoring previous config: " +
      +                     repr(e))
      +            return
      +        if not isinstance(new_conf, dict):
      +            showInfo("Invalid configuration, restoring previous config: "
      +                     "top level object must be a map")
      +            return
      +
      +        self.mgr["local"] = new_conf
      +        self.mgr.save(storage_name="local")
      +        super(ConfigEditor, self).accept()
      diff --git a/src/image_occlusion_enhanced/libaddon/anki/configmanager.py b/src/image_occlusion_enhanced/libaddon/anki/configmanager.py
      new file mode 100644
      index 00000000..62a8cb63
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/anki/configmanager.py
      @@ -0,0 +1,701 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Add-on configuration management
      +"""
      +
      +from __future__ import (absolute_import, division,
      +                        print_function, unicode_literals)
      +
      +import os
      +import io
      +
      +from anki.utils import json
      +from anki.hooks import addHook, runHook
      +
      +from .._vendor.packaging import version
      +
      +from ..utils import deepMergeDicts
      +from ..platform import ANKI20, PATH_THIS_ADDON, MODULE_ADDON
      +
      +DEFAULT_LOCAL_CONFIG_PATH = os.path.join(PATH_THIS_ADDON, "config.json")
      +DEFAULT_LOCAL_META_PATH = os.path.join(PATH_THIS_ADDON, "meta.json")
      +
      +
      +class ConfigError(Exception):
      +    """
      +    Thrown whenever a config-specific exception occurs
      +    """
      +    pass
      +
      +
      +class ConfigManager(object):
      +
      +    """
      +    Generic add-on configuration manager for Anki
      +
      +    Supports the following configuration storages:
      +
      +    name        location            data type     scope             notes
      +    ==========================================================================
      +    local       json files in       dictionary    all profiles      introduced
      +                add-on directory                                    in 2.1
      +    --------------------------------------------------------------------------
      +    synced      json string in      dictionary    user profile      limited
      +                collection.anki2                  (synced)          capacity
      +    --------------------------------------------------------------------------
      +    profile     pickle object       dictionary    user profile      limited
      +                in prefs.db                       (local)           capacity
      +
      +    """
      +
      +    _supported_storages = ("local", "synced", "profile")
      +
      +    def __init__(self, mw, config_dict={"local": None},
      +                 conf_key=MODULE_ADDON, conf_action=None,
      +                 reset_req=False, preload=False):
      +        """
      +        Initialize a new config manager object with the provided storages
      +        
      +        Defaults to initializing local storage.
      +
      +        Arguments:
      +            mw {QMainWindow} -- Anki main window object
      +
      +        Keyword Arguments:
      +            config_dict {dict}:
      +                Dictionary of configuration storages. Supported keys are
      +                limited to the ones listed in _supported_storages. Each
      +                key, with the exception of the local storage type, should
      +                be mapped to a dictionary of default config values.
      +                
      +                There is no need to supply a default dictionary for the
      +                local storage type, as it will automatically be read
      +                from the config.json file.
      +                (default: {{"local": None}})
      +            
      +            conf_key {str}:
      +                Dictionary key to use when saving storage types that use Anki's
      +                databases. Set to the topmost add-on module name by default.
      +                (e.g. "review_heatmap")
      +                (default: {MODULE_ADDON})
      +            
      +            conf_action {function}:
      +                Function/method to call when user clicks on configure button
      +                (2.1-specific) (default: {None})
      +            
      +            reset_req {bool}:
      +                Whether we should fire a reset event when the
      +                configuration is saved (e.g. to update parts of Anki's UI)
      +                (default: {False})
      +            
      +            preload {bool}:
      +                Whether or not to load all available configuration storages
      +                at profile load time. By default storages will only
      +                be loaded on demand. (default: {False})
      +        
      +        """
      +        self.mw = mw
      +        self._reset_req = reset_req
      +        self._conf_key = conf_key
      +        self._storages = {
      +            name: {
      +                "default": (default if name != "local"
      +                            else self._getLocalDefaults()),
      +                "dirty": False,
      +                "loaded": False
      +            }
      +            for name, default in config_dict.items()
      +        }
      +        
      +        self.conf_action = self.conf_updated_action = None
      +        self._setupAnkiHooks(conf_action=conf_action)
      +        self._setupCustomHooks()
      +        
      +        self._config = {}
      +        
      +        if preload:
      +            self._maybeLoad()
      +
      +    # Dictionary interface
      +    ######################################################################
      +
      +    def __getitem__(self, name):
      +        """
      +        Implements evaluation of self[storage_name]
      +
      +        storage_name needs to be in _supported_storages
      +
      +        Attempts to load storage on demand if it has not been
      +        initialized, yet.
      +
      +        Automatically falls back to defaults if no
      +        user-specific settings saved, yet.
      +        """
      +        self._checkStorage(name)
      +        try:
      +            config = self._config[name]
      +        except KeyError:
      +            # Attempt to load storage on demand
      +            self.load(storage_name=name)
      +            config = self._config[name]
      +        return config
      +
      +    def __setitem__(self, name, value):
      +        """
      +        Implements assignment of self[storage_name]
      +        """
      +        self._checkStorage(name)
      +        self._config[name] = value
      +        self._storages[name]["dirty"] = True
      +
      +    def __str__(self):
      +        """
      +        Returns printable representation of all config storage values.
      +        """
      +        return self._config.__str__()
      +
      +    # Attribute interface
      +    ######################################################################
      +    
      +    @property
      +    def local(self):
      +        return self.__getitem__("local")
      +    
      +    @local.setter
      +    def local(self, value):
      +        return self.__setitem__("local", value)
      +    
      +    @property
      +    def synced(self):
      +        return self.__getitem__("synced")
      +
      +    @synced.setter
      +    def synced(self, value):
      +        return self.__setitem__("synced", value)
      +    
      +    @property
      +    def profile(self):
      +        return self.__getitem__("profile")
      +
      +    @profile.setter
      +    def profile(self, value):
      +        return self.__setitem__("profile", value)
      +
      +    # Regular interface
      +    ######################################################################
      +
      +    def load(self, storage_name=None):
      +        """
      +        Load config values into ConfigManager.
      +
      +        Automatically falls back to defaults if no
      +        user-specific settings saved, yet.
      +
      +        Keyword Arguments:
      +            storage_name {str} -- Storage to load. Loads all storages if
      +                                  left blank (default: {None}).
      +        """
      +        for name in ([storage_name] if storage_name else self._storages):
      +            self._checkStorage(name)
      +            getter = getattr(self, "_get" + name.capitalize())
      +            self._config[name] = getter()
      +            self._storages[name]["loaded"] = True
      +
      +    def save(self, storage_name=None, profile_unload=False, reset=False):
      +        """
      +        Write config values to their corresponding storages.
      +
      +        Automatically fires a reset event if reset_req=True.
      +
      +        Keyword Arguments:
      +            storage_name {str} -- Storage to save. Saves all storages if
      +                                  left blank (default: {None}).
      +            profile_unload {bool} -- whether save has been triggered on profile
      +                                     unload
      +            reset {bool} -- whether to reset mw upon save (overwrites
      +                            reset_req instance attribute)
      +        """
      +        if storage_name:
      +            storages = [storage_name]  # limit to specific storage
      +        else:
      +            storages = self._storages
      +        
      +        for name in storages:
      +            self._checkStorage(name)
      +            saver = getattr(self, "_save" + name.capitalize())
      +            saver(self._config[name])
      +            self._storages[name]["dirty"] = False
      +        
      +        self.afterSave(reset=reset, profile_unload=profile_unload)
      +
      +    def afterSave(self, reset=False, profile_unload=False):
      +        """Trigger actions that are supposed to be run after saving config
      +        
      +        Keyword Arguments:
      +            profile_unload {bool} -- whether save has been triggered on profile
      +                                     unload
      +            reset {bool} -- whether to reset mw upon save (overwrites
      +                            reset_req instance attribute)
      +        """
      +        if (self._reset_req or reset) and not profile_unload:
      +            self.mw.reset()
      +
      +        if not profile_unload:
      +            runHook("config_saved_{}".format(self._conf_key))
      +
      +    @property
      +    def all(self):
      +        """
      +        Implements evaluation of self.all
      +
      +        Returns the values of all config storages currently managed
      +        by the config manager instance.
      +
      +        Returns:
      +            dict -- Dictionary of all config values
      +        """
      +        for storage in self._storages.values():
      +            if not storage["loaded"]:
      +                self.load()
      +                break
      +        return self._config
      +
      +    @all.setter
      +    def all(self, config_dict):
      +        """
      +        Implements assignment of self.all
      +
      +        Allows updating all configuration values at once.
      +
      +        Arguments:
      +            config_dict {dict}:
      +                Dictionary of config dictionaries
      +                (Same format as config_dict in __init__,
      +                only that the current config values should
      +                be provided instead of defaults)
      +        """
      +        self._config = config_dict
      +        # Reminder: setting self.all resets defaults, so it's important
      +        # that it's followed up by setting self.defaults
      +        # TODO: Think of a better way to handle this
      +        self._storages = {
      +            name: {"default": {}, "dirty": False, "loaded": False}
      +            for name in config_dict
      +        }
      +
      +    @property
      +    def defaults(self):
      +        """
      +        Implements evaluation of self.defaults
      +
      +        Returns the default values of all config storages
      +        currently managed by the config manager instance.
      +
      +        Returns:
      +            dict -- Dictionary of all default config values
      +        """
      +        return {name: storage_dict["default"]
      +                for name, storage_dict in self._storages.items()}
      +
      +    @defaults.setter
      +    def defaults(self, config_dict):
      +        """
      +        Implements assignment of self.defaults
      +
      +        Allows updating all default config values at once.
      +
      +        Arguments:
      +            config_dict {dict}:
      +                Dictionary of default config dictionaries
      +                (Same format as config_dict in __init__)
      +        """
      +        for name in config_dict:
      +            self._storages[name]["default"] = config_dict[name]
      +
      +    def restoreDefaults(self):
      +        """
      +        Restore all config values to the defaults and save storages
      +        """
      +        for name in self._storages:
      +            self._config[name] = self._storages[name]["default"]
      +        self.save()
      +
      +    def onProfileUnload(self):
      +        """
      +        Write unsaved changes to the corresponding storages.
      +        """
      +        for name, storage_dict in self._storages.items():
      +            if not storage_dict["dirty"]:
      +                continue
      +            
      +            try:
      +                self.save(name, profile_unload=True)
      +            except FileNotFoundError as e:
      +                # Corner case: Closing Anki after add-on uninstall
      +                # -> local config file no longer exists
      +                if name == "local":
      +                    print(e)
      +                    pass
      +                else:
      +                    raise
      +
      +    def setConfigAction(self, action):
      +        """
      +        Set function/method to call when user clicks on
      +        'Configure' button in Anki 2.1's add-on manager.
      +
      +        Arguments:
      +            action {function} -- Function to call
      +        """
      +        self.conf_action = action
      +        if not ANKI20 and action:
      +            self.mw.addonManager.setConfigAction(
      +                MODULE_ADDON, action)
      +
      +    def setConfigUpdatedAction(self, action):
      +        """
      +        Set function/method to call after config dialog is
      +        closed in Anki 2.1's add-on manager.
      +
      +        Arguments:
      +            action {function} -- Function to call
      +        """
      +        self.conf_updated_action = action
      +        if not ANKI20 and action:
      +            self.mw.addonManager.setConfigUpdatedAction(
      +                MODULE_ADDON, action)
      +
      +    # General helper methods
      +    ######################################################################
      +
      +    def _maybeLoad(self):
      +        """
      +        Try loading config storages, delegating loading until
      +        Anki profile is ready if necessary
      +        """
      +        if (any(i in self._storages for i in ("synced", "profile")) and
      +                self.mw.col is None):
      +            # Profile not ready. Defer config loading.
      +            addHook("profileLoaded", self.load)
      +            return
      +        self.load()
      +
      +    def _checkStorage(self, name):
      +        """
      +        Checks whether provided storage name is supported and
      +        initialized in current ConfigManager instance
      +
      +        Arguments:
      +            name {str} -- Storage name, as listed in _supported_storages
      +
      +        Raises:
      +            NotImplementedError -- Config storage not implemented in class
      +            ConfigError -- Config storage not initialized in current
      +                           instance
      +        """
      +        if name not in self._supported_storages:
      +            raise NotImplementedError(
      +                "Config storage type not implemented in libaddon: ", name)
      +        elif name not in self._storages:
      +            raise ConfigError(
      +                "Config storage type not available for this add-on: ", name)
      +
      +    def _setupCustomHooks(self):
      +        """
      +        Adds hooks for various events that should trigger saving the config
      +        """
      +        # Custom add-on-specifc hook that can be run by this/other add-ons
      +        addHook("config_changed_{}".format(self._conf_key),
      +                self.save)
      +        # Hook run on unloading Anki profile. Ensures that any unsaved changes
      +        # are saved to the corresponding storages
      +        addHook("unloadProfile", self.onProfileUnload)
      +
      +    def _setupAnkiHooks(self, conf_action):
      +        if "local" in self._storages:
      +            self.setConfigUpdatedAction(self.onLocalConfigUpdated)
      +            # TODO: setConfigAction to save local config before invoking
      +            # Anki's native config editor. Currently not feasible with
      +            # the existing config action implementation. NOTE: Make sure
      +            # to save local config when updating outside of config editor
      +        self.setConfigAction(conf_action)
      +        if ANKI20:
      +            self._setupAddonMenus20()
      +    
      +    def _setupAddonMenus20(self):
      +        from anki.hooks import wrap
      +        from aqt.addons import AddonManager
      +        from ..gui.dialog_configeditor import ConfigEditor
      +        
      +        from ..consts import ADDON
      +        from ..platform import PATH_ADDONS
      +        
      +        def onEdit(mgr, file_path, _old):
      +            entry_point = os.path.join(
      +                PATH_ADDONS, ADDON.NAME + ".py")
      +            if not file_path == entry_point:
      +                return _old(mgr, file_path)
      +            if self.conf_action:
      +                self.conf_action()
      +            elif "local" in self._config:
      +                ConfigEditor(self, self.mw)
      +            else:
      +                return _old(mgr, file_path)
      +        
      +        AddonManager.onEdit = wrap(AddonManager.onEdit, onEdit, "around")
      +
      +    # Local storage
      +    ######################################################################
      +
      +    def _getLocal(self):
      +        """
      +        Read local storage config from disk
      +
      +        Storage locations (add-on folder):
      +            - meta.json: user-specific
      +            - config.json: add-on defaults
      +
      +        Anki 2.1: Managed by Anki.
      +        Anki 2.0: Managed by ConfigManager.
      +
      +        Returns:
      +            dict -- Dictionary of config values
      +        """
      +        if not ANKI20:
      +            return self.mw.addonManager.getConfig(MODULE_ADDON)
      +        else:
      +            config = self._addonConfigDefaults20()
      +            meta = self._addonMeta20()
      +            user_conf = meta.get("config", {}) or {}
      +            config.update(user_conf)
      +            return config
      +
      +    def _getLocalDefaults(self):
      +        """
      +        Read default local storage config from disk
      +
      +        Returns:
      +            dict -- Dictionary of default config values
      +        """
      +        if not ANKI20:
      +            defaults = self.mw.addonManager.addonConfigDefaults(MODULE_ADDON)
      +            if defaults is None:
      +                raise ConfigError("Default config.json file could not be found")
      +        else:
      +            return self._addonConfigDefaults20()
      +
      +    def _saveLocal(self, config):
      +        """
      +        Save local storage config to disk
      +
      +        Arguments:
      +            dict -- Dictionary of local config values
      +        """
      +        if not ANKI20:
      +            self.mw.addonManager.writeConfig(MODULE_ADDON, config)
      +        else:
      +            self._writeAddonMeta20({"config": config})
      +
      +    def onLocalConfigUpdated(self, new_config):
      +        self._config["local"] = new_config
      +        self.afterSave()
      +
      +    # Synced storage
      +    ######################################################################
      +
      +    def _getSynced(self):
      +        """
      +        Read synced storage config from Anki collection object
      +
      +        Returns:
      +            dict -- Dictionary of synced config values
      +        """
      +        return self._getStorageObj("synced")[self._conf_key]
      +
      +    def _saveSynced(self, config):
      +        """
      +        Save synced storage config to Anki collection object
      +
      +        Arguments:
      +            dict -- Dictionary of synced config values
      +        """
      +        self._getStorageObj("synced")[self._conf_key] = config
      +        self.mw.col.setMod()
      +
      +    # Profile storage
      +    ######################################################################
      +
      +    def _getProfile(self):
      +        """
      +        Read profile storage config from Anki profile object
      +
      +        Returns:
      +            dict -- Dictionary of profile config values
      +        """
      +        return self._getStorageObj("profile")[self._conf_key]
      +
      +    def _saveProfile(self, config):
      +        """
      +        Save profile storage config to Anki profile object
      +
      +        Arguments:
      +            dict -- Dictionary of profile config values
      +        """
      +        self._getStorageObj("profile")[self._conf_key] = config
      +        self.mw.col.setMod()
      +
      +    # Helper methods for synced & profile storage
      +    ######################################################################
      +
      +    def _getStorageObj(self, name):
      +        """
      +        Get Anki storage dictionary for synced and profile storages.
      +        (e.g. mw.col.conf["review_heatmap"])
      +
      +        Storage objects:
      +            - synced: mw.col.conf
      +            - profile: mw.pm.profile
      +
      +        Arguments:
      +            name {str} -- Name of config storage
      +                          ("synced" or "profile")
      +
      +        Raises:
      +            NotImplementedError -- Config storage not supported
      +            ConfigError -- Config storage not ready, yet
      +
      +        Returns:
      +            dict -- Anki storage dictionary
      +        """
      +        conf_key = self._conf_key
      +        try:
      +            if name == "synced":
      +                storage_obj = self.mw.col.conf
      +            elif name == "profile":
      +                storage_obj = self.mw.pm.profile
      +            else:
      +                raise NotImplementedError(
      +                    "Storage object not implemented: ", name)
      +        except AttributeError:
      +            raise ConfigError("Config object is not ready, yet: ", name)
      +
      +        default_dict = self._storages[name]["default"]
      +
      +        # Initialize config
      +        if conf_key not in storage_obj:
      +            storage_obj[conf_key] = default_dict
      +        
      +        storage_dict = storage_obj[conf_key]
      +        dict_version = str(storage_dict.get("version", "0.0.0"))
      +        default_version = str(default_dict["version"])
      +
      +        # Upgrade config version if necessary
      +        if (version.parse(dict_version) < version.parse(default_version)):
      +            storage_obj[conf_key] = deepMergeDicts(
      +                default_dict, storage_dict, new=True)
      +            storage_obj[conf_key]["version"] = default_version
      +            self.mw.col.setMod()
      +
      +        return storage_obj
      +
      +    def _migrateStorage(self, src_storage, dst_storage):
      +        raise NotImplementedError()
      +
      +    # Helper methods for local storage on Anki 2.0
      +    ######################################################################
      +
      +    def _addonMeta20(self):
      +        """Get meta dictionary
      +
      +        Reads in meta.json in add-on folder and returns
      +        resulting dictionary of user-defined metadata values.
      +
      +        Note:
      +            Anki 2.1 stores both add-on meta data and customized
      +            settings in meta.json. In this module we are only dealing
      +            with the settings part.
      +
      +        Returns:
      +            dict: config dictionary
      +
      +        """
      +        try:
      +            meta = json.load(
      +                io.open(DEFAULT_LOCAL_META_PATH, encoding="utf-8"))
      +        except (IOError, OSError):
      +            meta = None
      +        except ValueError as e:
      +            print("Could not read meta.json: " + str(e))
      +            meta = None
      +
      +        if not meta:
      +            meta = {"config": self._addonConfigDefaults20()}
      +            self._writeAddonMeta20(meta)
      +
      +        return meta
      +
      +    def _writeAddonMeta20(self, meta):
      +        """Write meta dictionary
      +
      +        Writes meta dictionary to meta.json in add-on folder.
      +
      +        Args:
      +            meta (dict): meta dictionary
      +
      +        """
      +        with io.open(DEFAULT_LOCAL_META_PATH, 'w', encoding="utf-8") as f:
      +            content = json.dumps(meta, indent=4, sort_keys=True,
      +                                 ensure_ascii=False)
      +            f.write(unicode(content))  # noqa: F821
      +
      +    def _addonConfigDefaults20(self):
      +        """Get default config dictionary
      +
      +        Reads in config.json in add-on folder and returns
      +        resulting dictionary of default config values.
      +
      +        Returns:
      +            dict: config dictionary
      +
      +        Raises:
      +            ConfigError: If config.json cannot be parsed correctly.
      +                (The assumption being that we would end up in an
      +                inconsistent state if we were to return an empty
      +                config dictionary. This should never happen.)
      +
      +        """
      +        try:
      +            return json.load(io.open(DEFAULT_LOCAL_CONFIG_PATH,
      +                                     encoding="utf-8"))
      +        except (IOError, OSError, ValueError) as e:
      +            raise ConfigError("Config file could not be read: " + str(e))
      diff --git a/src/image_occlusion_enhanced/libaddon/anki/editor.py b/src/image_occlusion_enhanced/libaddon/anki/editor.py
      new file mode 100644
      index 00000000..082bdb4d
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/anki/editor.py
      @@ -0,0 +1,56 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Helpers for interacting with Anki's editor instances
      +"""
      +
      +# Handling async JS execution when saving editor content
      +
      +def editorSaveThen(callback):
      +    def onSaved(editor, *args, **kwargs):
      +        # uses evalWithCallback internally:
      +        editor.saveNow(lambda: callback(editor, *args, **kwargs))
      +    return onSaved
      +
      +
      +def widgetEditorSaveThen(callback):
      +    def onSaved(widget, *args, **kwargs):
      +        """[summary]
      +        
      +        Arguments:
      +            callback {[type]} -- [description]
      +            widget {Qt widget or widget} -- Qt object the editor is a member of
      +            (e.g. Browser, AddCards, EditCurrent)
      +        """
      +        # uses evalWithCallback internally:
      +        widget.editor.saveNow(lambda: callback(widget, *args, **kwargs))
      +    return onSaved
      diff --git a/src/image_occlusion_enhanced/libaddon/anki/search.py b/src/image_occlusion_enhanced/libaddon/anki/search.py
      new file mode 100644
      index 00000000..ff035ccc
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/anki/search.py
      @@ -0,0 +1,188 @@
      +# -*- coding: utf-8 -*-
      +
      +# Highlight Search Results in the Browser Add-on for Anki
      +#
      +# Copyright (C) 2017-2020  Aristotelis P. 
      +# Copyright (C) 2006-2020 Ankitects Pty Ltd and contributors
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +from enum import Enum
      +from typing import Union, List, Tuple, Dict
      +
      +
      +class QueryLanguageVersion(Enum):
      +    ANKI2100 = 0
      +    ANKI2124 = 1
      +
      +
      +class SearchTokenizer:
      +
      +    _operators_common: Tuple[str, ...] = ("or", "and", "+")
      +    _stripped_chars_common: str = '",*;'
      +    _ignored_values_common: Tuple[str, ...] = ("*", "_", "_*")
      +
      +    _ignored_tags_common: Tuple[str, ...] = (
      +        # default query language:
      +        "added:",
      +        "deck:",
      +        "note:",
      +        "tag:",
      +        "mid:",
      +        "nid:",
      +        "cid:",
      +        "card:",
      +        "is:",
      +        "flag:",
      +        "rated:",
      +        "dupe:",
      +        "prop:",
      +        # added by add-ons:
      +        "seen:",
      +        "rid:",
      +    )
      +
      +    _ignored_tags_by_version: Dict[QueryLanguageVersion, Tuple[str, ...]] = {
      +        QueryLanguageVersion.ANKI2100: tuple(),
      +        QueryLanguageVersion.ANKI2124: ("re:", "nc:"),
      +    }
      +
      +    _quotes_by_version: Dict[QueryLanguageVersion, Tuple[str, ...]] = {
      +        QueryLanguageVersion.ANKI2100: ('"',),
      +        QueryLanguageVersion.ANKI2124: ('"', "'"),
      +    }
      +
      +    def __init__(
      +        self,
      +        query_language_version: QueryLanguageVersion = QueryLanguageVersion.ANKI2124,
      +    ):
      +        self._query_language_version = query_language_version
      +        self._ignored_values = self._ignored_values_common
      +        self._operators = self._operators_common
      +        self._stripped_chars = self._stripped_chars_common
      +        self._ignored_tags = (
      +            self._ignored_tags_common
      +            + self._ignored_tags_by_version[query_language_version]
      +        )
      +        self._quotes = self._quotes_by_version[query_language_version]
      +
      +    def tokenize(self, query: str) -> List[str]:
      +        """
      +        Tokenize search string
      +
      +        Based on finder code in Anki versions 2.1.23 and lower
      +        (anki.find.Finder._tokenize)
      +        """
      +
      +        _escape_supported = (
      +            self._query_language_version.value >= QueryLanguageVersion.ANKI2124.value
      +        )
      +
      +        in_quote: Union[bool, str] = False
      +        in_escape: bool = False
      +        tokens: List[str] = []
      +        token: str = ""
      +
      +        for c in query:
      +            # quoted text
      +            if c in self._quotes:
      +                if in_quote:
      +                    if c == in_quote and not in_escape:
      +                        in_quote = False
      +                    else:
      +                        token += c
      +                elif token:
      +                    # quotes are allowed to start directly after a :
      +                    if token[-1] == ":":
      +                        in_quote = c
      +                    else:
      +                        token += c
      +                else:
      +                    in_quote = c
      +            # escaped characters
      +            elif c == "\\" and _escape_supported:
      +                if in_escape:
      +                    # escaped "\"
      +                    token += c
      +                    in_escape = False
      +                else:
      +                    in_escape = True
      +            # separator (space and ideographic space)
      +            elif c in (" ", "\u3000"):
      +                if in_quote:
      +                    token += c
      +                elif token:
      +                    # space marks token finished
      +                    tokens.append(token)
      +                    token = ""
      +            # nesting
      +            elif c in ("(", ")"):
      +                if in_quote:
      +                    token += c
      +                else:
      +                    if c == ")" and token:
      +                        tokens.append(token)
      +                        token = ""
      +                    tokens.append(c)
      +            # negation
      +            elif c == "-":
      +                if token:
      +                    token += c
      +                elif not tokens or tokens[-1] != "-":
      +                    tokens.append("-")
      +            # normal character
      +            else:
      +                in_escape = False
      +                token += c
      +        # if we finished in a token, add it
      +        if token:
      +            tokens.append(token)
      +
      +        return tokens
      +
      +    def get_searchable_tokens(self, tokens: List[str]) -> List[str]:
      +        searchable_tokens: List[str] = []
      +
      +        for token in tokens:
      +            if (
      +                token in self._operators
      +                or token.startswith("-")
      +                or token.startswith(self._ignored_tags)
      +            ):
      +                continue
      +
      +            if ":" in token:
      +                value = token.split(":", 1)[1]
      +                if not value or value in self._ignored_values:
      +                    continue
      +            else:
      +                value = token
      +
      +            value = value.strip(self._stripped_chars)
      +
      +            searchable_tokens.append(value)
      +
      +        return searchable_tokens
      diff --git a/src/image_occlusion_enhanced/libaddon/anki/utils.py b/src/image_occlusion_enhanced/libaddon/anki/utils.py
      new file mode 100644
      index 00000000..925bca90
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/anki/utils.py
      @@ -0,0 +1,66 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Utility functions for interacting with Anki
      +"""
      +
      +import os
      +
      +from aqt import mw
      +
      +from ..platform import ANKI20, PATH_ADDONS
      +from ..consts import ADDON
      +
      +
      +def debugInfo():
      +    """Return verbose info on add-ons and Anki installation"""
      +    info = ["{name} version {version}".format(name=ADDON.NAME,
      +                                              version=ADDON.VERSION)]
      +    if ANKI20:
      +        from aqt.qt import QT_VERSION_STR, PYQT_VERSION_STR
      +        from aqt import appVersion
      +        from anki.utils import platDesc
      +        info.append("Anki {version} (Qt {qt} PyQt {pyqt})".format(
      +            version=appVersion, qt=QT_VERSION_STR, pyqt=PYQT_VERSION_STR))
      +        info.append(platDesc())
      +        files = [f for f in os.listdir(PATH_ADDONS)
      +                 if f.endswith(".py")]
      +        info.append("Add-ons:\n\n" + repr(files))
      +    else:
      +        from aqt.utils import supportText
      +        info.append(supportText())
      +
      +        addmgr = mw.addonManager
      +        info.append("Add-ons:\n\n" + "\n".join(
      +            addmgr.annotatedName(d) for d in addmgr.allAddons()))
      +
      +    return "\n\n".join(info)
      diff --git a/src/image_occlusion_enhanced/libaddon/config/abstract/anki.py b/src/image_occlusion_enhanced/libaddon/config/abstract/anki.py
      new file mode 100644
      index 00000000..76e4839a
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/abstract/anki.py
      @@ -0,0 +1,197 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Add-on configuration storages
      +"""
      +
      +import copy
      +from abc import ABC, abstractmethod
      +
      +from anki.hooks import addHook, remHook
      +from aqt.main import AnkiQt
      +
      +from ..._vendor.packaging import version
      +from ...anki.additions.hooks import HOOKS
      +from ...util.nesting import deepMergeDicts
      +from ..errors import (
      +    ConfigError,
      +    ConfigNotLoadedError,
      +    ConfigNotReadyError,
      +    FutureConfigError,
      +)
      +from .base import ConfigStorage
      +
      +from typing import Optional
      +
      +__all__ = [
      +    "AnkiConfigStorage",
      +]
      +
      +
      +# TODO: SUBCLASS DOCSTRINGS
      +
      +
      +class AnkiConfigStorage(ConfigStorage, ABC):
      +
      +    name = ""
      +    root_namespace = None
      +
      +    def __init__(
      +        self,
      +        mw: AnkiQt,
      +        namespace: str,
      +        defaults: dict,
      +        atomic: bool = False,
      +    ):
      +        try:
      +            _ = defaults["version"]
      +        except KeyError:
      +            raise ConfigError("Defaults need to include a 'version' key/value pair")
      +
      +        super().__init__(
      +            mw, namespace, defaults=defaults, atomic=atomic
      +        )
      +
      +        self._deferred: bool = False
      +
      +    def initialize(self) -> bool:
      +        if self._loaded:
      +            return True
      +        self._ready = True
      +        try:
      +            self.load()
      +        except ConfigNotReadyError:
      +            self._deferInitialization()
      +            return False
      +        return super().initialize()
      +
      +    def load(self) -> bool:
      +        """[summary]
      +
      +        Returns:
      +            bool -- Whether existing config was found
      +        """
      +        if not self._ready:
      +            raise ConfigNotReadyError("Attempted to load before initializing config")
      +        config_object = self._configObject
      +        user_data = config_object.get(self._namespace, None)
      +        if user_data:
      +            user_data = self._getUpdatedConfig(user_data, self.defaults)
      +        self.data = user_data or copy.deepcopy(self._defaults)
      +        super().load()
      +        return bool(user_data)
      +
      +    def save(self) -> None:
      +        if not self._loaded:
      +            raise ConfigNotLoadedError("Attempted to save before loading config")
      +        # Ensure that we pass values instead of a reference to our data:
      +        self._configObject[self._namespace] = copy.deepcopy(self.data)
      +        self._flush()
      +        return super().save()
      +
      +    def delete(self) -> None:
      +        self.data = {}
      +        self.save()
      +        return super().delete()
      +
      +    def purge(self) -> None:
      +        """Completely remove modifications from base storage object"""
      +        try:
      +            del self._configObject[self._namespace]
      +        except KeyError:
      +            raise ConfigError("Attempted to purge non-existing config")
      +        self._flush()
      +
      +    def unload(self) -> None:
      +        if self._deferred:
      +            remHook("profileLoaded", self.initialize)
      +        self._deferred = False
      +        super().unload()
      +
      +    def _deferInitialization(self):
      +        if self._deferred:
      +            raise ConfigError("Initialization already deferred")
      +        self._deferred = True
      +        addHook(HOOKS.PROFILE_LOADED, self.initialize)
      +
      +    @property
      +    def _configObject(self) -> dict:
      +        try:
      +            config_object = self._ankiConfigObject()
      +        except AttributeError:
      +            config_object = None
      +        if config_object is None:
      +            raise ConfigNotReadyError("Anki base storage object is not ready")
      +
      +        if self.root_namespace:
      +            try:
      +                config_object = config_object[self.root_namespace]
      +            except KeyError:
      +                config_object[self.root_namespace] = {}
      +
      +        return config_object
      +
      +    @staticmethod
      +    def _getUpdatedConfig(data: dict, defaults: dict) -> Optional[dict]:
      +        try:
      +            defaults_version = defaults["version"]
      +        except KeyError:
      +            raise ConfigError("Defaults need to include a 'version' key/value pair")
      +
      +        # legacy support: non-str version or no version
      +        data_version = str(data.get("version", "0.0.0"))
      +
      +        parsed_version_data = version.parse(data_version)
      +        parsed_version_defaults = version.parse(defaults_version)
      +
      +        # Upgrade config version if necessary
      +        if parsed_version_data < parsed_version_defaults:
      +            data = deepMergeDicts(
      +                defaults, data, new=True
      +            )  # returns deepcopied defaults, updated with data
      +            data["version"] = defaults_version
      +        elif parsed_version_data > parsed_version_defaults:
      +            # TODO: Figure out where to handle
      +            raise FutureConfigError("Config is newer than add-on release")
      +        else:
      +            # ensure that we never operate on base config object directly
      +            data = copy.deepcopy(data)
      +
      +        return data
      +
      +    @abstractmethod
      +    def _ankiConfigObject(self) -> dict:
      +        pass
      +
      +    @abstractmethod
      +    def _flush(self) -> None:
      +        pass
      diff --git a/src/image_occlusion_enhanced/libaddon/config/abstract/base.py b/src/image_occlusion_enhanced/libaddon/config/abstract/base.py
      new file mode 100644
      index 00000000..4ac88e37
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/abstract/base.py
      @@ -0,0 +1,167 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Add-on configuration storages
      +"""
      +
      +from abc import ABC
      +import copy
      +
      +from anki.hooks import addHook, remHook
      +from aqt.main import AnkiQt
      +
      +from ..errors import ConfigNotLoadedError, ConfigError
      +from ..signals import ConfigSignals
      +from .interface import ConfigInterface
      +
      +from typing import Any, Optional, Hashable
      +
      +
      +__all__ = ["ConfigStorage"]
      +
      +
      +class ConfigStorage(ConfigInterface, ABC):
      +
      +    name: str = ""
      +
      +    def __init__(
      +        self,
      +        mw: AnkiQt,
      +        namespace: Optional[str] = None,
      +        defaults: Optional[dict] = None,
      +        atomic: bool = False,
      +    ):
      +        super().__init__()
      +
      +        self._mw = mw
      +        self._namespace = namespace
      +        self._defaults = defaults or {}
      +        self._atomic = False
      +
      +        self._ready: bool = False
      +        self._loaded: bool = False
      +        self._dirty: bool = False
      +
      +        self.data = {}
      +        self.signals = ConfigSignals()
      +
      +    # Overwrite some ConfigInterface implementations
      +
      +    def __getitem__(self, key: Hashable) -> Any:
      +        if not self._loaded:
      +            raise ConfigNotLoadedError()
      +        return super().__getitem__(key)
      +
      +    def __setitem__(self, key: Hashable, value: Any) -> None:
      +        if not self._loaded:
      +            raise ConfigNotLoadedError()
      +        super().__setitem__(key, value)
      +        if self._atomic:
      +            self.save()
      +        else:
      +            self._dirty = True
      +
      +    # Fill out ConfigInterface abstract methods and properties
      +
      +    @property
      +    def ready(self) -> bool:
      +        return self._ready
      +
      +    @property
      +    def loaded(self) -> bool:
      +        return self._loaded
      +
      +    @property
      +    def dirty(self) -> bool:
      +        return self._dirty
      +
      +    # TODO: CRUCIAL – perform config validation
      +    # if invalid:
      +    #   config.reset()
      +    #   and perhaps notify user
      +    # CONSIDER: perform these only at load/save time or with every access?
      +    # (expensive!)
      +
      +    def initialize(self) -> bool:
      +        """Performs one-shot setup steps. Should only be fired once.
      +        Separated out of __init__ in order to provide more granular control
      +        of initialization steps, and enable deferring some initialization
      +        steps if necessary
      +        """
      +        addHook("unloadProfile", self.unload)
      +        self._ready = True
      +        self.signals.initialized.emit()
      +        return True
      +
      +    def load(self) -> bool:
      +        # should set self.data from base storage
      +        self._loaded = True
      +        self.signals.loaded.emit()
      +        return True
      +
      +    def save(self) -> None:
      +        # should set base storage from self.data
      +        self._dirty = False
      +        self.signals.saved.emit()
      +
      +    @property
      +    def defaults(self) -> dict:
      +        return self._defaults
      +
      +    @defaults.setter
      +    def defaults(self, data: dict) -> None:
      +        self._defaults = copy.deepcopy(data)
      +
      +    def reset(self) -> None:
      +        self.data = self.defaults
      +        self.save()
      +        self.signals.reset.emit()
      +
      +    def delete(self) -> None:
      +        # data representation and base storage object are emptied, but persist
      +        # (e.g. don't completely purge storage key out of storage object)
      +        self._dirty = False
      +        self.signals.deleted.emit()
      +
      +    def unload(self):
      +        self.signals.unloaded.emit()
      +        # TODO: is this necessary? throws errors for now ↓
      +        # self.signals.disconnect()
      +        if not self._loaded:
      +            return
      +        try:
      +            self.save()
      +        except (FileNotFoundError, ConfigError) as e:
      +            # Corner case: Closing Anki after add-on uninstall
      +            print(e)
      +        self._loaded = self._dirty = self._ready = False
      +        remHook("unloadProfile", self.unload)
      diff --git a/src/image_occlusion_enhanced/libaddon/config/abstract/interface.py b/src/image_occlusion_enhanced/libaddon/config/abstract/interface.py
      new file mode 100644
      index 00000000..7b711998
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/abstract/interface.py
      @@ -0,0 +1,99 @@
      +from abc import abstractmethod, abstractproperty
      +from collections.abc import MutableMapping
      +
      +from ..signals import ConfigSignals
      +
      +from typing import Any, Hashable
      +
      +
      +class ConfigInterface(MutableMapping):
      +
      +    signals: ConfigSignals
      +    data: dict
      +
      +    # Fill out MutableMapping interface abstract methods
      +
      +    def __getitem__(self, key: Hashable) -> Any:
      +        if key in self.data:
      +            return self.data[key]
      +        if hasattr(self.__class__, "__missing__"):
      +            return self.__class__.__missing__(self, key)  # type: ignore
      +        raise KeyError(key)
      +
      +    def __setitem__(self, key: Hashable, value: Any) -> None:
      +        self.data[key] = value
      +
      +    def __delitem__(self, key: Hashable) -> None:
      +        del self.data[key]
      +
      +    def __iter__(self):
      +        return iter(self.data)
      +
      +    def __len__(self):
      +        return len(self.data)
      +
      +    def __contains__(self, key: Hashable):
      +        return key in self.data
      +
      +    def __repr__(self) -> str:
      +        return repr(self.data)
      +
      +    # Define additional abstract methods and properties
      +
      +    @abstractproperty
      +    def defaults(self) -> dict:
      +        return {}
      +
      +    @abstractproperty
      +    def ready(self) -> bool:
      +        """Base storage object ready for I/O
      +
      +        Returns:
      +            bool -- whether base storage object is ready
      +        """
      +        return False
      +
      +    @abstractproperty
      +    def loaded(self) -> bool:
      +        """Config loaded from base storage object
      +
      +        Returns:
      +            bool -- whether config is loaded in
      +        """
      +        return False
      +
      +    @abstractproperty
      +    def dirty(self) -> bool:
      +        """Config representation diverges from base storage object
      +
      +        Returns:
      +            bool -- whether config diverges from base storage object
      +        """
      +        return False
      +
      +    @abstractmethod
      +    def initialize(self) -> bool:
      +        """Performs one-shot setup steps. Should only be fired once.
      +        Separated out of __init__ in order to provide more granular control
      +        of initialization steps, and enable deferring some initialization
      +        steps if necessary
      +        """
      +        # should emit signals.initialized
      +        return
      +
      +    @abstractmethod
      +    def load(self) -> bool:
      +        # should emit signals.loaded
      +        return
      +
      +    @abstractmethod
      +    def save(self):
      +        pass
      +
      +    @abstractmethod
      +    def reset(self):
      +        pass
      +
      +    @abstractmethod
      +    def delete(self):
      +        pass
      diff --git a/src/image_occlusion_enhanced/libaddon/config/errors.py b/src/image_occlusion_enhanced/libaddon/config/errors.py
      new file mode 100644
      index 00000000..b093cf89
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/errors.py
      @@ -0,0 +1,48 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +
      +class ConfigError(Exception):
      +    """
      +    Thrown whenever a config-specific exception occurs
      +    """
      +
      +    pass
      +
      +class FutureConfigError(ConfigError):
      +    pass
      +
      +class ConfigNotReadyError(ConfigError):
      +    pass
      +
      +
      +class ConfigNotLoadedError(ConfigError):
      +    pass
      diff --git a/src/image_occlusion_enhanced/libaddon/config/manager.py b/src/image_occlusion_enhanced/libaddon/config/manager.py
      new file mode 100644
      index 00000000..e6424fb6
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/manager.py
      @@ -0,0 +1,123 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +from .abstract.base import ConfigStorage
      +from .abstract.interface import ConfigInterface
      +from .errors import ConfigError
      +from .signals import ConfigSignals
      +
      +from typing import List, Dict
      +
      +
      +class ConfigManager(ConfigInterface):
      +    def __init__(self, storages: List[ConfigStorage]) -> None:
      +        self.data: Dict[str, ConfigStorage] = {
      +            storage.name: storage for storage in storages
      +        }
      +        self.signals = ConfigSignals()
      +        self._unloaded: set = set()
      +
      +    # Overwrite some ConfigInterface implementations
      +
      +    def __getitem__(self, key: str) -> ConfigStorage:
      +        return super().__getitem__(key)
      +
      +    def __setitem__(self, key: str, value: ConfigStorage):
      +        try:
      +            assert isinstance(value, ConfigStorage)
      +        except AssertionError:
      +            raise ConfigError("Value to be set needs to be a valid ConfigStorage")
      +        return super().__setitem__(key, value)
      +
      +    # Fill out ConfigInterface abstract methods and properties
      +
      +    @property
      +    def ready(self) -> bool:
      +        return all(storage.ready for storage in self.data.values())
      +
      +    @property
      +    def loaded(self) -> bool:
      +        return all(storage.loaded for storage in self.data.values())
      +
      +    @property
      +    def dirty(self) -> bool:
      +        return any(storage.dirty for storage in self.data.values())
      +
      +    def initialize(self) -> bool:
      +        for storage in self.data.values():
      +            storage.initialize()
      +            storage.signals.unloaded.connect(lambda: self._markUnloaded(storage.name))
      +        self.signals.initialized.emit()
      +        return True
      +
      +    def load(self) -> bool:
      +        for storage in self.data.values():
      +            storage.load()
      +        self.signals.loaded.emit()
      +        return True
      +
      +    def save(self) -> None:
      +        for storage in self.data.values():
      +            storage.save()
      +        self.signals.saved.emit()
      +
      +    @property
      +    def defaults(self) -> dict:
      +        return {storage.name: storage.defaults for storage in self.data.values()}
      +
      +    @defaults.setter
      +    def defaults(self, data: Dict[str, dict]) -> None:
      +        for storage_name in data:
      +            try:
      +                storage = self.data[storage_name]
      +            except KeyError:
      +                raise ConfigError(f"Unsupported storage {storage_name}")
      +            storage.defaults = data[storage_name]
      +
      +    def reset(self) -> None:
      +        for storage in self.data.values():
      +            storage.reset()
      +        self.signals.reset.emit()
      +
      +    def delete(self) -> None:
      +        for storage in self.data.values():
      +            storage.delete()
      +        self.signals.deleted.emit()
      +
      +    def unload(self):
      +        for storage in self.data.values():
      +            storage.unload()
      +        self.signals.unloaded.emit()
      +
      +    def _markUnloaded(self, storage_name: str):
      +        self._unloaded.add(storage_name)
      +        if all(k in self._unloaded for k in self.data.keys()):
      +            self.signals.unloaded.emit()
      diff --git a/src/image_occlusion_enhanced/libaddon/config/manager_old.py b/src/image_occlusion_enhanced/libaddon/config/manager_old.py
      new file mode 100644
      index 00000000..0ca3d25e
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/manager_old.py
      @@ -0,0 +1,582 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Add-on configuration management
      +"""
      +
      +from anki.hooks import addHook, runHook
      +
      +from .._vendor.packaging import version
      +
      +from ..util.nesting import deepMergeDicts
      +
      +from ..addon import ADDON
      +
      +class ConfigError(Exception):
      +    """
      +    Thrown whenever a config-specific exception occurs
      +    """
      +    pass
      +
      +
      +class ConfigManager:
      +
      +    """
      +    Generic add-on configuration manager for Anki
      +
      +    Supports the following configuration storages:
      +
      +    name        location            data type     scope             notes
      +    ==========================================================================
      +    local       json files in       dictionary    all profiles      introduced
      +                add-on directory                                    in 2.1
      +    --------------------------------------------------------------------------
      +    synced      json string in      dictionary    user profile      limited
      +                collection.anki2                  (synced)          capacity
      +    --------------------------------------------------------------------------
      +    profile     pickle object       dictionary    user profile      limited
      +                in prefs.db                       (local)           capacity
      +
      +    """
      +
      +    _supported_storages = ("local", "synced", "profile")
      +
      +    def __init__(self, mw, config_dict={"local": None},
      +                 conf_key=ADDON.MODULE, conf_action=None,
      +                 reset_req=False, preload=False):
      +        """
      +        Initialize a new config manager object with the provided storages
      +
      +        Defaults to initializing local storage.
      +
      +        Arguments:
      +            mw {QMainWindow} -- Anki main window object
      +
      +        Keyword Arguments:
      +            config_dict {dict}:
      +                Dictionary of configuration storages. Supported keys are
      +                limited to the ones listed in _supported_storages. Each
      +                key, with the exception of the local storage type, should
      +                be mapped to a dictionary of default config values.
      +
      +                There is no need to supply a default dictionary for the
      +                local storage type, as it will automatically be read
      +                from the config.json file.
      +                (default: {{"local": None}})
      +
      +            conf_key {str}:
      +                Dictionary key to use when saving storage types that use Anki's
      +                databases. Set to the topmost add-on module name by default.
      +                (e.g. "review_heatmap")
      +                (default: {ADDON.MODULE})
      +
      +            conf_action {function}:
      +                Function/method to call when user clicks on configure button
      +                (2.1-specific) (default: {None})
      +
      +            reset_req {bool}:
      +                Whether we should fire a reset event when the
      +                configuration is saved (e.g. to update parts of Anki's UI)
      +                (default: {False})
      +
      +            preload {bool}:
      +                Whether or not to load all available configuration storages
      +                at profile load time. By default storages will only
      +                be loaded on demand. (default: {False})
      +
      +        """
      +        self.mw = mw
      +        self._reset_req = reset_req
      +        self._conf_key = conf_key
      +        self._storages = {
      +            name: {
      +                "default": (default if name != "local"
      +                            else self._getLocalDefaults()),
      +                "dirty": False,
      +                "loaded": False
      +            }
      +            for name, default in config_dict.items()
      +        }
      +
      +        self.conf_action = self.conf_updated_action = None
      +        self._setupAnkiHooks(conf_action=conf_action)
      +        self._setupCustomHooks()
      +
      +        self._config = {}
      +
      +        if preload:
      +            self._maybeLoad()
      +
      +    # Dictionary interface
      +    ######################################################################
      +
      +    def __getitem__(self, name):
      +        """
      +        Implements evaluation of self[storage_name]
      +
      +        storage_name needs to be in _supported_storages
      +
      +        Attempts to load storage on demand if it has not been
      +        initialized, yet.
      +
      +        Automatically falls back to defaults if no
      +        user-specific settings saved, yet.
      +        """
      +        self._checkStorage(name)
      +        try:
      +            config = self._config[name]
      +        except KeyError:
      +            # Attempt to load storage on demand
      +            self.load(storage_name=name)
      +            config = self._config[name]
      +        return config
      +
      +    def __setitem__(self, name, value):
      +        """
      +        Implements assignment of self[storage_name]
      +        """
      +        self._checkStorage(name)
      +        self._config[name] = value
      +        self._storages[name]["dirty"] = True
      +
      +    def __str__(self):
      +        """
      +        Returns printable representation of all config storage values.
      +        """
      +        return self._config.__str__()
      +
      +    # Attribute interface
      +    ######################################################################
      +
      +    @property
      +    def local(self):
      +        return self.__getitem__("local")
      +
      +    @local.setter
      +    def local(self, value):
      +        return self.__setitem__("local", value)
      +
      +    @property
      +    def synced(self):
      +        return self.__getitem__("synced")
      +
      +    @synced.setter
      +    def synced(self, value):
      +        return self.__setitem__("synced", value)
      +
      +    @property
      +    def profile(self):
      +        return self.__getitem__("profile")
      +
      +    @profile.setter
      +    def profile(self, value):
      +        return self.__setitem__("profile", value)
      +
      +    # Regular interface
      +    ######################################################################
      +
      +    def load(self, storage_name=None):
      +        """
      +        Load config values into ConfigManager.
      +
      +        Automatically falls back to defaults if no
      +        user-specific settings saved, yet.
      +
      +        Keyword Arguments:
      +            storage_name {str} -- Storage to load. Loads all storages if
      +                                  left blank (default: {None}).
      +        """
      +        for name in ([storage_name] if storage_name else self._storages):
      +            self._checkStorage(name)
      +            getter = getattr(self, "_get" + name.capitalize())
      +            self._config[name] = getter()
      +            self._storages[name]["loaded"] = True
      +
      +    def save(self, storage_name=None, profile_unload=False, reset=False):
      +        """
      +        Write config values to their corresponding storages.
      +
      +        Automatically fires a reset event if reset_req=True.
      +
      +        Keyword Arguments:
      +            storage_name {str} -- Storage to save. Saves all storages if
      +                                  left blank (default: {None}).
      +            profile_unload {bool} -- whether save has been triggered on profile
      +                                     unload
      +            reset {bool} -- whether to reset mw upon save (overwrites
      +                            reset_req instance attribute)
      +        """
      +        if storage_name:
      +            storages = [storage_name]  # limit to specific storage
      +        else:
      +            storages = self._storages
      +
      +        for name in storages:
      +            self._checkStorage(name)
      +            saver = getattr(self, "_save" + name.capitalize())
      +            saver(self._config[name])
      +            self._storages[name]["dirty"] = False
      +
      +        self.afterSave(reset=reset, profile_unload=profile_unload)
      +
      +    def afterSave(self, reset=False, profile_unload=False):
      +        """Trigger actions that are supposed to be run after saving config
      +
      +        Keyword Arguments:
      +            profile_unload {bool} -- whether save has been triggered on profile
      +                                     unload
      +            reset {bool} -- whether to reset mw upon save (overwrites
      +                            reset_req instance attribute)
      +        """
      +        if (self._reset_req or reset) and not profile_unload:
      +            self.mw.reset()
      +
      +        if not profile_unload:
      +            runHook("config_saved_{}".format(self._conf_key))
      +
      +    @property
      +    def all(self):
      +        """
      +        Implements evaluation of self.all
      +
      +        Returns the values of all config storages currently managed
      +        by the config manager instance.
      +
      +        Returns:
      +            dict -- Dictionary of all config values
      +        """
      +        for storage in self._storages.values():
      +            if not storage["loaded"]:
      +                self.load()
      +                break
      +        return self._config
      +
      +    @all.setter
      +    def all(self, config_dict):
      +        """
      +        Implements assignment of self.all
      +
      +        Allows updating all configuration values at once.
      +
      +        Arguments:
      +            config_dict {dict}:
      +                Dictionary of config dictionaries
      +                (Same format as config_dict in __init__,
      +                only that the current config values should
      +                be provided instead of defaults)
      +        """
      +        self._config = config_dict
      +        # Reminder: setting self.all resets defaults, so it's important
      +        # that it's followed up by setting self.defaults
      +        # TODO: Think of a better way to handle this
      +        self._storages = {
      +            name: {"default": {}, "dirty": False, "loaded": False}
      +            for name in config_dict
      +        }
      +
      +    @property
      +    def defaults(self):
      +        """
      +        Implements evaluation of self.defaults
      +
      +        Returns the default values of all config storages
      +        currently managed by the config manager instance.
      +
      +        Returns:
      +            dict -- Dictionary of all default config values
      +        """
      +        return {name: storage_dict["default"]
      +                for name, storage_dict in self._storages.items()}
      +
      +    @defaults.setter
      +    def defaults(self, config_dict):
      +        """
      +        Implements assignment of self.defaults
      +
      +        Allows updating all default config values at once.
      +
      +        Arguments:
      +            config_dict {dict}:
      +                Dictionary of default config dictionaries
      +                (Same format as config_dict in __init__)
      +        """
      +        for name in config_dict:
      +            self._storages[name]["default"] = config_dict[name]
      +
      +    def restoreDefaults(self):
      +        """
      +        Restore all config values to the defaults and save storages
      +        """
      +        for name in self._storages:
      +            self._config[name] = self._storages[name]["default"]
      +        self.save()
      +
      +    def onProfileUnload(self):
      +        """
      +        Write unsaved changes to the corresponding storages.
      +        """
      +        for name, storage_dict in self._storages.items():
      +            if not storage_dict["dirty"]:
      +                continue
      +
      +            try:
      +                self.save(name, profile_unload=True)
      +            except FileNotFoundError as e:
      +                # Corner case: Closing Anki after add-on uninstall
      +                # -> local config file no longer exists
      +                if name == "local":
      +                    print(e)
      +                    pass
      +                else:
      +                    raise
      +
      +    def setConfigAction(self, action):
      +        """
      +        Set function/method to call when user clicks on
      +        'Configure' button in Anki 2.1's add-on manager.
      +
      +        Arguments:
      +            action {function} -- Function to call
      +        """
      +        self.conf_action = action
      +        if not action:
      +            return
      +        self.mw.addonManager.setConfigAction(
      +            ADDON.MODULE, action)
      +
      +    def setConfigUpdatedAction(self, action):
      +        """
      +        Set function/method to call after config dialog is
      +        closed in Anki 2.1's add-on manager.
      +
      +        Arguments:
      +            action {function} -- Function to call
      +        """
      +        self.conf_updated_action = action
      +        if not action:
      +            return
      +        self.mw.addonManager.setConfigUpdatedAction(
      +            ADDON.MODULE, action)
      +
      +    # General helper methods
      +    ######################################################################
      +
      +    def _maybeLoad(self):
      +        """
      +        Try loading config storages, delegating loading until
      +        Anki profile is ready if necessary
      +        """
      +        if (any(i in self._storages for i in ("synced", "profile")) and
      +                self.mw.col is None):
      +            # Profile not ready. Defer config loading.
      +            addHook("profileLoaded", self.load)
      +            return
      +        self.load()
      +
      +    def _checkStorage(self, name):
      +        """
      +        Checks whether provided storage name is supported and
      +        initialized in current ConfigManager instance
      +
      +        Arguments:
      +            name {str} -- Storage name, as listed in _supported_storages
      +
      +        Raises:
      +            NotImplementedError -- Config storage not implemented in class
      +            ConfigError -- Config storage not initialized in current
      +                           instance
      +        """
      +        if name not in self._supported_storages:
      +            raise NotImplementedError(
      +                "Config storage type not implemented in libaddon: ", name)
      +        elif name not in self._storages:
      +            raise ConfigError(
      +                "Config storage type not available for this add-on: ", name)
      +
      +    def _setupCustomHooks(self):
      +        """
      +        Adds hooks for various events that should trigger saving the config
      +        """
      +        # Custom add-on-specifc hook that can be run by this/other add-ons
      +        addHook("config_changed_{}".format(self._conf_key),
      +                self.save)
      +        # Hook run on unloading Anki profile. Ensures that any unsaved changes
      +        # are saved to the corresponding storages
      +        addHook("unloadProfile", self.onProfileUnload)
      +
      +    def _setupAnkiHooks(self, conf_action):
      +        if "local" in self._storages:
      +            self.setConfigUpdatedAction(self.onLocalConfigUpdated)
      +            # TODO: setConfigAction to save local config before invoking
      +            # Anki's native config editor. Currently not feasible with
      +            # the existing config action implementation. NOTE: Make sure
      +            # to save local config when updating outside of config editor
      +        self.setConfigAction(conf_action)
      +
      +    # Local storage
      +    ######################################################################
      +
      +    def _getLocal(self):
      +        """
      +        Read local storage config from disk
      +
      +        Storage locations (add-on folder):
      +            - meta.json: user-specific
      +            - config.json: add-on defaults
      +
      +        Returns:
      +            dict -- Dictionary of config values
      +        """
      +        return self.mw.addonManager.getConfig(ADDON.MODULE)
      +
      +    def _getLocalDefaults(self):
      +        """
      +        Read default local storage config from disk
      +
      +        Returns:
      +            dict -- Dictionary of default config values
      +        """
      +        return self.mw.addonManager.addonConfigDefaults(ADDON.MODULE)
      +
      +    def _saveLocal(self, config):
      +        """
      +        Save local storage config to disk
      +
      +        Arguments:
      +            dict -- Dictionary of local config values
      +        """
      +        self.mw.addonManager.writeConfig(ADDON.MODULE, config)
      +
      +    def onLocalConfigUpdated(self, new_config):
      +        self._config["local"] = new_config
      +        self.afterSave()
      +
      +    # Synced storage
      +    ######################################################################
      +
      +    def _getSynced(self):
      +        """
      +        Read synced storage config from Anki collection object
      +
      +        Returns:
      +            dict -- Dictionary of synced config values
      +        """
      +        return self._getStorageObj("synced")[self._conf_key]
      +
      +    def _saveSynced(self, config):
      +        """
      +        Save synced storage config to Anki collection object
      +
      +        Arguments:
      +            dict -- Dictionary of synced config values
      +        """
      +        self._getStorageObj("synced")[self._conf_key] = config
      +        self.mw.col.setMod()
      +
      +    # Profile storage
      +    ######################################################################
      +
      +    def _getProfile(self):
      +        """
      +        Read profile storage config from Anki profile object
      +
      +        Returns:
      +            dict -- Dictionary of profile config values
      +        """
      +        return self._getStorageObj("profile")[self._conf_key]
      +
      +    def _saveProfile(self, config):
      +        """
      +        Save profile storage config to Anki profile object
      +
      +        Arguments:
      +            dict -- Dictionary of profile config values
      +        """
      +        self._getStorageObj("profile")[self._conf_key] = config
      +        self.mw.col.setMod()
      +
      +    # Helper methods for synced & profile storage
      +    ######################################################################
      +
      +    def _getStorageObj(self, name):
      +        """
      +        Get Anki storage dictionary for synced and profile storages.
      +        (e.g. mw.col.conf["review_heatmap"])
      +
      +        Storage objects:
      +            - synced: mw.col.conf
      +            - profile: mw.pm.profile
      +
      +        Arguments:
      +            name {str} -- Name of config storage
      +                          ("synced" or "profile")
      +
      +        Raises:
      +            NotImplementedError -- Config storage not supported
      +            ConfigError -- Config storage not ready, yet
      +
      +        Returns:
      +            dict -- Anki storage dictionary
      +        """
      +        conf_key = self._conf_key
      +        try:
      +            if name == "synced":
      +                storage_obj = self.mw.col.conf
      +            elif name == "profile":
      +                storage_obj = self.mw.pm.profile
      +            else:
      +                raise NotImplementedError(
      +                    "Storage object not implemented: ", name)
      +        except AttributeError:
      +            raise ConfigError("Config object is not ready, yet: ", name)
      +
      +        default_dict = self._storages[name]["default"]
      +
      +        # Initialize config
      +        if conf_key not in storage_obj:
      +            storage_obj[conf_key] = default_dict
      +
      +        storage_dict = storage_obj[conf_key]
      +        dict_version = str(storage_dict.get("version", "0.0.0"))
      +        default_version = str(default_dict["version"])
      +
      +        # Upgrade config version if necessary
      +        if (version.parse(dict_version) < version.parse(default_version)):
      +            storage_obj[conf_key] = deepMergeDicts(
      +                default_dict, storage_dict, new=True)
      +            storage_obj[conf_key]["version"] = default_version
      +            self.mw.col.setMod()
      +
      +        return storage_obj
      +
      +    def _migrateStorage(self, src_storage, dst_storage):
      +        raise NotImplementedError()
      diff --git a/src/image_occlusion_enhanced/libaddon/config/signals.py b/src/image_occlusion_enhanced/libaddon/config/signals.py
      new file mode 100644
      index 00000000..67561102
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/signals.py
      @@ -0,0 +1,41 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +from PyQt5.QtCore import pyqtSignal, QObject
      +
      +
      +class ConfigSignals(QObject):
      +    initialized = pyqtSignal()
      +    saved = pyqtSignal()
      +    loaded = pyqtSignal()
      +    reset = pyqtSignal()
      +    deleted = pyqtSignal()
      +    unloaded = pyqtSignal()
      diff --git a/src/image_occlusion_enhanced/libaddon/config/storages/__init__.py b/src/image_occlusion_enhanced/libaddon/config/storages/__init__.py
      new file mode 100644
      index 00000000..854cc5f4
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/storages/__init__.py
      @@ -0,0 +1,34 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Add-on configuration storages
      +"""
      diff --git a/src/image_occlusion_enhanced/libaddon/config/storages/anki.py b/src/image_occlusion_enhanced/libaddon/config/storages/anki.py
      new file mode 100644
      index 00000000..10ea8a03
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/storages/anki.py
      @@ -0,0 +1,90 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Add-on configuration storages
      +"""
      +
      +from ..abstract.anki import AnkiConfigStorage
      +from ..errors import ConfigNotReadyError
      +
      +__all__ = [
      +    "ProfileConfigStorage",
      +    "SyncedConfigStorage",
      +    "MetaConfigStorage",
      +    "LibaddonMetaConfigStorage",
      +]
      +
      +
      +class ProfileConfigStorage(AnkiConfigStorage):
      +    # NOTE: Profile is available at add-on init time when Anki is launched
      +    # with a specific profile as a parameter. This is usually not the case,
      +    # but might arise when testing an add-on and lead you astray.
      +
      +    name = "profile"
      +
      +    def _ankiConfigObject(self) -> dict:
      +        return self._mw.pm.profile
      +
      +    def _flush(self) -> None:
      +        # no flushing required
      +        pass
      +
      +
      +class SyncedConfigStorage(AnkiConfigStorage):
      +
      +    name = "synced"
      +
      +    def _ankiConfigObject(self) -> dict:
      +        return self._mw.col.conf
      +
      +    def _flush(self) -> None:
      +        try:
      +            self._mw.col.setMod()
      +        except AttributeError:
      +            raise ConfigNotReadyError("Anki base storage object is not ready")
      +
      +
      +class MetaConfigStorage(AnkiConfigStorage):
      +
      +    name = "meta"
      +
      +    def _ankiConfigObject(self) -> dict:
      +        return self._mw.pm.meta
      +
      +    def _flush(self) -> None:
      +        # no flushing required
      +        pass
      +
      +
      +class LibaddonMetaConfigStorage(MetaConfigStorage):
      +
      +    root_namespace = "libaddon"
      diff --git a/src/image_occlusion_enhanced/libaddon/config/storages/json.py b/src/image_occlusion_enhanced/libaddon/config/storages/json.py
      new file mode 100644
      index 00000000..5b24714e
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/storages/json.py
      @@ -0,0 +1,156 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Add-on configuration storages
      +"""
      +
      +import json
      +from pathlib import Path
      +
      +from aqt.main import AnkiQt
      +
      +from ...util.types import PathOrString
      +from ..abstract.base import ConfigStorage
      +from ..errors import ConfigError, ConfigNotReadyError, ConfigNotLoadedError
      +
      +from typing import Optional
      +
      +__all__ = ["JSONConfigStorage"]
      +
      +
      +class JSONConfigStorage(ConfigStorage):
      +    """e.g. JSON file in user_data folder"""
      +
      +    name = "json"
      +
      +    def __init__(
      +        self,
      +        mw: AnkiQt,
      +        path: PathOrString,
      +        defaults: Optional[dict] = None,
      +        atomic: bool = False,
      +    ):
      +        self._path: Path = Path(path)
      +        super().__init__(mw, defaults=defaults, atomic=atomic)
      +
      +    def initialize(self) -> bool:
      +        if self._loaded:
      +            return True
      +        self._ready = True
      +        self.load()
      +        return super().initialize()
      +
      +    def load(self) -> bool:
      +        if not self._ready:
      +            raise ConfigNotReadyError("Attempted to load before initializing config")
      +        path = self._safePath(self._path)
      +        data = self._readData(path)
      +        self.data = data if data is not None else self.defaults
      +        super().load()
      +        return data is not None
      +
      +    def save(self) -> None:
      +        if not self._loaded:
      +            raise ConfigNotLoadedError("Attempted to save before loading config")
      +        path = self._safePath(self._path)
      +        self._writeData(path, self.data)
      +        super().save()
      +
      +    def delete(self):
      +        self.data = {}
      +        self.save()
      +        super().delete()
      +
      +    def purge(self) -> None:
      +        """Completely remove modifications from base storage object"""
      +        self._removeFile()
      +
      +    def _safePath(self, path: Path) -> Path:
      +        if not path.is_file():
      +            path.parent.mkdir(parents=True, exist_ok=True)
      +            with path.open("w", encoding="utf-8") as f:
      +                json.dump(None, f)
      +        return path
      +
      +    def _readData(self, path: Path) -> Optional[dict]:
      +        try:
      +            with path.open(encoding="utf-8") as f:
      +                return json.load(f)
      +        except (IOError, OSError, ValueError) as e:
      +            # log
      +            raise ConfigError(
      +                f"Could not read {self.name} storage at {path}:\n{str(e)}"
      +            )
      +
      +    def _writeData(self, path: Path, data: dict) -> None:
      +        try:
      +            with path.open("w", encoding="utf-8") as f:
      +                json.dump(data, f)
      +        except (IOError, OSError, ValueError) as e:
      +            # log
      +            raise ConfigError(
      +                f"Could not write to {self.name} storage at {path}:\n{str(e)}"
      +            )
      +
      +    def _removeFile(self) -> None:
      +        path = self._safePath(self._path)
      +        path.unlink()
      +    
      +    def unload(self) -> None:
      +        # FIXME: overwrites ConfigStorage.unload to prevent
      +        # unloading on profile switch. not necessary for JSONConfigStorage
      +        # since config shared across profiles. Instead we just perform a
      +        # (more safe) save on config unload
      +        if not self._loaded:
      +            return
      +        try:
      +            self.save()
      +        except (FileNotFoundError, ConfigError) as e:
      +            # Corner case: Closing Anki after add-on uninstall
      +            print(e)
      +
      +
      +class UserFilesConfigStorage(JSONConfigStorage):
      +    def __init__(
      +        self,
      +        mw: AnkiQt,
      +        file_stem: str,
      +        defaults: Optional[dict] = None,
      +        atomic: bool = False,
      +    ):
      +        from ...platform import pathUserFiles
      +
      +        path = Path(pathUserFiles()) / f"{file_stem}.json"
      +
      +        super().__init__(
      +            mw, path, defaults=defaults, atomic=atomic
      +        )
      diff --git a/src/image_occlusion_enhanced/libaddon/config/storages/local.py b/src/image_occlusion_enhanced/libaddon/config/storages/local.py
      new file mode 100644
      index 00000000..fe313426
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/config/storages/local.py
      @@ -0,0 +1,136 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2020  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Add-on configuration storages
      +"""
      +
      +from aqt.main import AnkiQt
      +
      +from ...addon import ADDON
      +from ...anki import ANKI
      +from ...util.version import checkVersion
      +from ..abstract.base import ConfigStorage
      +from ..errors import ConfigError, ConfigNotReadyError, ConfigNotLoadedError
      +
      +__all__ = ["LocalConfigStorage"]
      +
      +
      +class LocalConfigStorage(ConfigStorage):
      +
      +    name = "local"
      +
      +    def __init__(
      +        self,
      +        mw: AnkiQt,
      +        atomic: bool = False,
      +        namespace=ADDON.MODULE,
      +        native_gui: bool = True,
      +    ):
      +        self._native_gui = native_gui
      +        
      +        # Anki handles defaults:
      +        defaults = mw.addonManager.addonConfigDefaults(namespace)
      +        if defaults is None:
      +            raise ConfigError("No default config file provided")
      +
      +        super().__init__(
      +            mw, namespace, defaults=defaults, atomic=atomic
      +        )
      +
      +    def initialize(self) -> bool:
      +        if self._loaded:
      +            return True
      +        self._ready = True
      +        self.load()
      +        if self._native_gui:
      +            self._ensureSaveBeforeConfigGUILoaded()
      +            self._ensureLoadAfterConfigGUIFinished()
      +        return super().initialize()
      +
      +    def delete(self) -> None:
      +        self.data = {}
      +        self.save()
      +        return super().delete()
      +
      +    def load(self) -> bool:
      +        if not self._ready:
      +            raise ConfigNotReadyError("Attempted to load before initializing config")
      +        data = self._mw.addonManager.getConfig(self._namespace)
      +        if data is None:  # should never happen
      +            raise ConfigError("No default config file provided")
      +        self.data = data
      +        return super().load()
      +
      +    def save(self) -> None:
      +        if not self._loaded:
      +            raise ConfigNotLoadedError("Attempted to save before loading config")
      +        self._mw.addonManager.writeConfig(self._namespace, self.data)
      +        return super().save()
      +
      +    @property
      +    def defaults(self) -> dict:
      +        return self._defaults
      +
      +    @defaults.setter
      +    def defaults(self, data: dict) -> None:
      +        raise NotImplementedError(
      +            f"{self.name} storage does not support setting defaults"
      +        )
      +
      +    def _ensureLoadAfterConfigGUIFinished(self) -> None:
      +        self._mw.addonManager.setConfigUpdatedAction(self._namespace, self.load)
      +
      +    def _ensureSaveBeforeConfigGUILoaded(self) -> None:
      +        """ugly workaround, drop as soon as possible"""
      +
      +        if checkVersion(ANKI.VERSION, "2.1.17"):
      +            self._mw.addonManager.setConfigAction(
      +                self._namespace, self._saveBeforeConfigLoaded
      +            )
      +            return
      +
      +        from anki.hooks import wrap
      +        from aqt.addons import AddonsDialog
      +
      +        def wrappedOnConfig(addonsDialog: AddonsDialog, *args, **kwargs):
      +            """Save before config editor is invoked"""
      +            addon = addonsDialog.onlyOneSelected()
      +            if not addon or addon != self._namespace:
      +                return
      +            self.save()
      +
      +        AddonsDialog.onConfig = wrap(AddonsDialog.onConfig, wrappedOnConfig, "before")
      +
      +    def _saveBeforeConfigLoaded(self) -> bool:
      +        self.save()
      +        # instructs Anki to continue with config dialog:
      +        return False
      diff --git a/src/image_occlusion_enhanced/libaddon/consts.py b/src/image_occlusion_enhanced/libaddon/consts.py
      new file mode 100644
      index 00000000..e4856e32
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/consts.py
      @@ -0,0 +1,71 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Package-wide constants
      +"""
      +
      +from __future__ import (absolute_import, division,
      +                        print_function, unicode_literals)
      +
      +
      +def setAddonProperties(addon):
      +    """Update ADDON class properties from another ADDON class
      +    
      +    Arguments:
      +        addon {object} -- an ADDON class with properties stored as class
      +                          attributes
      +    """
      +    for key, value in addon.__dict__.items():
      +        if key.startswith("__") and key.endswith("__"):
      +            # ignore special attributes
      +            continue
      +        setattr(ADDON, key, value)
      +
      +class ADDON(object):
      +    """Class storing general add-on properties
      +    Property names need to be all-uppercase with no leading underscores.
      +    Should be updated by add-on on initialization.
      +    """
      +    NAME = ""
      +    MODULE = ""
      +    REPO = ""
      +    ID = ""
      +    VERSION = ""
      +    LICENSE = ""
      +    AUTHORS = ()
      +    AUTHOR_MAIL = ""
      +    LIBRARIES = ()
      +    CONTRIBUTORS = ()
      +    SPONSORS = ()
      +    MEMBERS_CREDITED = ()
      +    MEMBERS_TOP = ()
      +    LINKS = {}
      diff --git a/src/image_occlusion_enhanced/libaddon/debug.py b/src/image_occlusion_enhanced/libaddon/debug.py
      new file mode 100644
      index 00000000..fab14d8d
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/debug.py
      @@ -0,0 +1,151 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Log add-on events
      +"""
      +
      +import os
      +import sys
      +from datetime import datetime
      +
      +# need to vendorize 'logging' as Anki's 'logging' does not contain handlers
      +from ._vendor import logging
      +from ._vendor.logging import handlers
      +
      +from .consts import ADDON
      +from .anki.utils import debugInfo
      +from .platform import PATH_THIS_ADDON
      +
      +__all__ = [
      +    "logger", "enableDebugging", "disableDebugging", "maybeStartDebugging",
      +    "startDebugging", "PATH_LOG"
      +]
      +
      +
      +PATH_LOG = os.path.join(PATH_THIS_ADDON, "log.txt")
      +
      +logger = logging.getLogger(ADDON.MODULE)
      +
      +_cli_handler = logging.StreamHandler(sys.stdout)
      +_file_handler = handlers.RotatingFileHandler(
      +    PATH_LOG, maxBytes=2000000, backupCount=1, delay=True)
      +
      +_fmt = ("%(asctime)s %(filename)s:%(funcName)s:%(lineno)-8s "
      +        "%(levelname)-8s: %(message)s")
      +_fmt_date = '%Y-%m-%dT%H:%M:%S%z'
      +
      +_formatter = logging.Formatter(_fmt, _fmt_date)
      +_file_handler.setFormatter(_formatter)
      +_cli_handler.setFormatter(_formatter)
      +
      +logger.addHandler(_file_handler)
      +logger.addHandler(_cli_handler)
      +
      +logger.setLevel(logging.ERROR)
      +
      +PATH_DEBUG_ENABLER = os.path.join(PATH_THIS_ADDON, "debug")
      +
      +
      +def isDebuggingOn():
      +    return logger.level == logging.DEBUG
      +
      +
      +def debugFileSet():
      +    return os.path.exists(PATH_DEBUG_ENABLER)
      +
      +
      +def toggleDebugging():
      +    if debugFileSet():
      +        disableDebugging()
      +        return False
      +    else:
      +        enableDebugging()
      +        return True
      +
      +
      +def enableDebugging():
      +    if debugFileSet():
      +        return
      +    with open(PATH_DEBUG_ENABLER, "w"):
      +        pass
      +    if not isDebuggingOn():
      +        startDebugging()
      +
      +
      +def disableDebugging():
      +    if not debugFileSet():
      +        return
      +    os.remove(PATH_DEBUG_ENABLER)
      +    if isDebuggingOn():
      +        stopDebugging()
      +
      +
      +def maybeStartDebugging():
      +    if not debugFileSet():
      +        return
      +    startDebugging()
      +
      +
      +def startDebugging():
      +    logger.setLevel(logging.DEBUG)
      +    time = datetime.today().strftime(_fmt_date)
      +    logger.info("="*79)
      +    logger.info(22 * " " + "START {name} log {time}".format(
      +        name=ADDON.NAME, time=time) + 22 * " ")
      +    logger.info("="*79)
      +    logger.info(debugInfo())
      +    logger.info("="*79)
      +
      +
      +def stopDebugging():
      +    logger.setLevel(logging.ERROR)
      +
      +
      +def getLatestLog():
      +    if not os.path.exists(PATH_LOG):
      +        return False
      +    with open(PATH_LOG, "r") as f:
      +        log = f.read()
      +    return log
      +
      +
      +def openLog():
      +    if not os.path.exists(PATH_LOG):
      +        return False
      +    from .utils import openFile
      +    openFile(PATH_LOG)
      +
      +def clearLog():
      +    if not os.path.exists(PATH_LOG):
      +        return False
      +    with open(PATH_LOG, "w") as f:
      +        f.write("")
      diff --git a/src/image_occlusion_enhanced/libaddon/gui/__init__.py b/src/image_occlusion_enhanced/libaddon/gui/__init__.py
      new file mode 100644
      index 00000000..cb4da6e5
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/gui/__init__.py
      @@ -0,0 +1,34 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Package for custom reusable Qt dialogs (Anki-specific)
      +"""
      diff --git a/src/image_occlusion_enhanced/libaddon/gui/about.py b/src/image_occlusion_enhanced/libaddon/gui/about.py
      new file mode 100644
      index 00000000..5e9ca062
      --- /dev/null
      +++ b/src/image_occlusion_enhanced/libaddon/gui/about.py
      @@ -0,0 +1,155 @@
      +# -*- coding: utf-8 -*-
      +
      +# Libaddon for Anki
      +#
      +# Copyright (C) 2018-2019  Aristotelis P. 
      +#
      +# This program is free software: you can redistribute it and/or modify
      +# it under the terms of the GNU Affero General Public License as
      +# published by the Free Software Foundation, either version 3 of the
      +# License, or (at your option) any later version, with the additions
      +# listed at the end of the license file that accompanied this program.
      +#
      +# This program is distributed in the hope that it will be useful,
      +# but WITHOUT ANY WARRANTY; without even the implied warranty of
      +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +# GNU Affero General Public License for more details.
      +#
      +# You should have received a copy of the GNU Affero General Public License
      +# along with this program.  If not, see .
      +#
      +# NOTE: This program is subject to certain additional terms pursuant to
      +# Section 7 of the GNU Affero General Public License.  You should have
      +# received a copy of these additional terms immediately following the
      +# terms and conditions of the GNU Affero General Public License that
      +# accompanied this program.
      +#
      +# If not, please request a copy through one of the means of contact
      +# listed here: .
      +#
      +# Any modifications to this file must keep this entire header intact.
      +
      +"""
      +Generate 'about' info, including credits, copyright, etc.
      +"""
      +
      +from __future__ import (absolute_import, division,
      +                        print_function, unicode_literals)
      +
      +from ..consts import ADDON
      +from ..platform import ANKI20
      +
      +if not ANKI20:
      +    string = str
      +else:
      +    import string
      +
      +libs_header = (
      +    "

      {} ships with the following third-party code:

      ".format(ADDON.NAME)) + +html_template = """\ + + + + + + + {title} +

      Credits

      + {authors_string} + {libs_string} + +

      Thank you!

      +

      My heartfelt thanks go out to everyone who has supported this add-on through their tips, + contributions, or any other means. You guys rock!

      +

      In particular I would like to thank all of the awesome people who support me + on Patreon, including:

      +
      {members_string}
      +

      Want to be listed here? + Pledge your support on Patreon now + to receive all kinds of exclusive goodies! +

      + +

      License

      +

      {display_name} is free and open-source software. The add-on code that runs within + Anki is released under the {license} license, extended by a number of additional terms. + For more information please see the license file that accompanied this program.

      +

      This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. + Please see the license file for more details.

      + + {debugging} + +\ +""" + +debugging_template = """\ +

      Debugging

      +

      Please don't use any of the following features unless instructed to:

      +\ +""" + +authors_template = """\ +

      © {years} {name}

      \ +""" + +libs_item_template = """\ +
    • {name} ({version}), © {author}, {license}
    • \ +""" + +title_template = """\ +

      About {display_name} (v{version})

      \ +""" + +def getAboutString(title=False, showDebug=False): + authors_string = "\n".join(authors_template.format(**dct) + for dct in ADDON.AUTHORS) + libs_entries = "\n".join(libs_item_template.format(**dct) + for dct in ADDON.LIBRARIES) + if libs_entries: + libs_string = "\n".join((libs_header, "
        ", libs_entries, "
      ")) + else: + libs_string = "" + contributors_string = "

      With patches from: {}

      ".format( + ", ".join(sorted(ADDON.CONTRIBUTORS, key=string.lower)) + ) + + members_top_string = "{}".format(", ".join(ADDON.MEMBERS_TOP)) + members_credited_string = ", ".join(ADDON.MEMBERS_CREDITED) + members_string = "

      {t},

      {r}

      ".format( + t=members_top_string, r=members_credited_string) + + if title: + title_string = title_template.format(display_name=ADDON.NAME, + version=ADDON.VERSION) + else: + title_string = "" + + if showDebug: + debugging = debugging_template + else: + debugging = "" + + return html_template.format(display_name=ADDON.NAME, + license=ADDON.LICENSE, + title=title_string, + authors_string=authors_string, + libs_string=libs_string, + contributors_string=contributors_string, + members_string=members_string, + qrc_prefix=ADDON.MODULE, + debugging=debugging) diff --git a/src/image_occlusion_enhanced/libaddon/gui/basic/__init__.py b/src/image_occlusion_enhanced/libaddon/gui/basic/__init__.py new file mode 100644 index 00000000..62ce1420 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/basic/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Package for custom reusable Qt dialogs (not Anki-specific) +""" diff --git a/src/image_occlusion_enhanced/libaddon/gui/basic/dialog_basic.py b/src/image_occlusion_enhanced/libaddon/gui/basic/dialog_basic.py new file mode 100644 index 00000000..1fbc4dcd --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/basic/dialog_basic.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Basic QDialog, extended with some quality-of-life improvements +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from .widgets.qt import * +from .interface import CommonWidgetInterface + +__all__ = ["BasicDialog"] + + +class BasicDialog(QDialog): + + def __init__(self, form_module=None, parent=None, **kwargs): + super(BasicDialog, self).__init__(parent=parent, **kwargs) + self.parent = parent + self.interface = CommonWidgetInterface(self) + # Set up UI from pre-generated UI form: + if form_module: + self.form = form_module.Ui_Dialog() + self.form.setupUi(self) + self._setupUI() + self._setupEvents() + self._setupShortcuts() + + # WIDGET SET-UP + + def _setupUI(self): + """ + Set up any type of subsequent UI modifications + (e.g. adding custom widgets on top of form) + """ + pass + + def _setupEvents(self): + """Set up any type of event bindings""" + pass + + def _setupShortcuts(self): + """Set up any type of keyboard shortcuts""" + pass + + # DIALOG OPEN/CLOSE + + def _onClose(self): + """Executed whenever dialog closed""" + pass + + def _onAccept(self): + """Executed only if dialog confirmed""" + pass + + def _onReject(self): + """Executed only if dialog dismissed""" + pass + + def accept(self): + """Overwrites default accept() to control close actions""" + self._onClose() + self._onAccept() + super(BasicDialog, self).accept() + + def reject(self): + """Overwrites default reject() to control close actions""" + self._onClose() + self._onReject() + super(BasicDialog, self).reject() diff --git a/src/image_occlusion_enhanced/libaddon/gui/basic/dialog_mapped.py b/src/image_occlusion_enhanced/libaddon/gui/basic/dialog_mapped.py new file mode 100644 index 00000000..38a923ea --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/basic/dialog_mapped.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Simple dialog with support for mapping widget state from/to dictionary +keys and/or setter/getter methods. +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from ...utils import getNestedValue, setNestedValue + +from .widgets.qt import * +from .dialog_basic import BasicDialog + +__all__ = ["MappedDialog"] + + +class MappedDialog(BasicDialog): + + def __init__(self, mapped_widgets, data, defaults, + form_module=None, parent=None, **kwargs): + """ + Simple dialog with support for mapping widget state from/to dictionary + keys and/or setter/getter methods. + + Arguments: + mapped_widgets {sequence} -- A list/tuple of mappings between + widget names, dictionary keys, and + special methods to act as mediators + (see below for specs) + data {dict} -- Dictionary containing user data + defaults {dict} -- Dictionary containing default data + + Keyword Arguments: + form_module {PyQt form module} -- Dialog form module generated + through pyuic (default: {None}) + parent {QWidget} -- Parent Qt widget (default: {None}) + + --- mapped_widgets specifications --- + + mapped_widgets should consist of a sequence (list or tuple) of tuples + of the form: + + > ("widget_object_name", property_mapping_tuple) + + where widget_object_name is the valid object name of a widget + found in the current dialog, or a qualified dot-separated attribute + path leading to it (e.g. "form.selHmlCol" for self.form.selHmCol) + + Each property mapping tuple should be phrased as follows: + + > ("property_descriptor", assignment_dictionary) + + where property_descriptor is a valid name as per the keys defined + in CommonWidgetInterface.methods_by_key + (as of writing: "value", "items", "current", "min", "max") + + The key, value pairs defined in the assignment dictionary determine + the way in which config values are applied to and read from their + corresponding widgets. + + The following key, value pairs are supported: + + "dataPath" {tuple} -- Sequence of dictionary keys / sequence + indices pointing to valid entry + in the data and defaults dictionaries + (e.g. ("synced", "mode", 1) for getting + self.data["synced"]["mode"][1]) ) + "setter" {str} -- Name of method called to either process + config value before being applied to the + widget property, or to return a config value + through other means + "getter" {str} -- Name of method called to either process + widget value before being applied to the + configuration, or to return a widget value + through other means + + Only the following combinations of the above are supported: + + "dataPath" only: + Values are read from and written to self.config + with no processing applied + "dataPath" and "setter" / "getter": + Values are processed after reading and/or before writing + "setter" / "getter": + Reading and/or writing the values is delegated to the + provided methods + + The string values provided for the "setter" and "getter" keys + describe instance methods of this class or classes inheriting + from it. + + In summary, an example of a valid mapped_widgets object could + look as follows: + + > ( + > ("form.dateLimData", ( + > ("value", { + > "dataPath": ("synced", "limdate") + > }), + > ("min", { + > "setter": "_setDateLimDataMIn" + > }), + > ("max", { + > "setter": "_setDateLimDataMax" + > }), + > )), + > ("form.selHmCalMode", ( + > ("items", { + > "setter": "_setSelHmCalModeItems" + > }), + > ("value", { + > "dataPath": ("synced", "mode"), + > "setter": "_setselHmCalModeValue" + > }), + > )) + > ) + """ + super(MappedDialog, self).__init__(form_module=form_module, + parent=parent, **kwargs) + self._mapped_widgets = mapped_widgets + self._defaults = defaults + self._data = data + self.setData(data) + + # API + + def setData(self, data): + for widget_name, properties in self._mapped_widgets: + for key, property_dict in properties: + value = self._dataToWidgetVal(data, property_dict) + self.interface.set(widget_name, key, value) + + def getData(self): + for widget_name, properties in self._mapped_widgets: + for key, property_dict in properties: + data_path = self._dataPathToList( + property_dict.get("dataPath", "")) + if not data_path: # property irrelevant for config + continue + widget_val = self.interface.get(widget_name, key) + self._widgetToDataVal(self._data, property_dict, widget_val, + data_path) + return self._data + + def restoreData(self): + self.setData(self._defaults) + + # Events + + def _setupEvents(self): + super(MappedDialog, self)._setupEvents() + if getattr(self.form, "buttonBox", None): + restore_btn = self.form.buttonBox.button( + QDialogButtonBox.RestoreDefaults) + if restore_btn: + restore_btn.clicked.connect(self.restoreData) + + # Utility functions to translate data into widget state and vice versa + + def _dataPathToList(self, path): + if not path: + return [] + crumbs = path.split("/") + return [c if not c.strip("-").isdigit() else + int(c.strip("-")) * (-1 if c.startswith("-") else 1) + for c in crumbs] + + def _dataToWidgetVal(self, data, property_dict): + """ + Get value from config and translate it to valid widget + value, optionally pre-processing it using defined + setter method + + Arguments: + data {dict} -- Dictionary of user config values + property_dict {dict} -- Dictionary describing widget <-> config + mappping + + Returns: + object -- Valid value for widget + """ + data_path = self._dataPathToList( + property_dict.get("dataPath", "")) + setter_name = property_dict.get("setter", "") + setter = getattr(self, setter_name, None) if setter_name else None + data_val = getNestedValue(data, data_path) if data_path else None + + if setter is not None: + widget_val = setter(data_val) + else: + widget_val = data_val + + return widget_val + + def _widgetToDataVal(self, data, property_dict, widget_val, data_path): + """ + Get widget state/value and translate it to valid + config value, optionally pre-processing it using defined + getter method + + Arguments: + property_dict {dict} -- Dictionary describing widget <-> config + mappping + widget_val {object} -- Current widget value + + Returns: + tuple -- tuple of data_path {tuple} and data_val {object} + """ + getter_name = property_dict.get("getter", None) + getter = getattr(self, getter_name, None) if getter_name else None + + if getter: + data_val = getter(widget_val) + else: + data_val = widget_val + + setNestedValue(data, data_path, data_val) diff --git a/src/image_occlusion_enhanced/libaddon/gui/basic/interface.py b/src/image_occlusion_enhanced/libaddon/gui/basic/interface.py new file mode 100644 index 00000000..c4d9935f --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/basic/interface.py @@ -0,0 +1,1088 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Common interface to Qt widgets + +Implements a number of common API calls that unify changing and reading +state from various Qt widgets. This allows for easier translation +between stored values and widget state, while also catching +type errors and other problems early on. + +Subclassing each respective Qt widget would be the more elegant way, +but that is not feasible when primarily working with Qt designer generated +UIs. +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from collections import MutableSequence, MutableSet, MutableMapping + +from ...utils import getNestedAttribute +from ...platform import PYTHON3 + +from .widgets.qt import * +from .widgets.qkeygrabber import QKeyGrabButton +from .widgets.qcolorbutton import QColorButton +# TODO: Switch to QKeySequenceEdit once Qt4 support dropped +# TODO: add support for QSlider + +__all__ = ["CommonWidgetInterface"] + +# Variables used for type checks +MUTABLES = (MutableSequence, MutableSet, MutableMapping) +STRINGTYPES = (str,) if PYTHON3 else (str, unicode) +NUMERICTYPES = (int, float) +LISTTYPES = (list, tuple) + + +class CommonWidgetInterface(object): + """ + Common interface to Qt widgets + + Implements a number of common API calls that unify changing and reading + state from various Qt widgets. This allows for easier translation + between stored values and widget state. + + Arguments: + parent {QWidget} -- Qt parent widget whose children we want to control + with this interface (e.g. a QDialog instance) + + Raises: + NotImplementedError -- In case of an unimplemented API call + AssertionErorr -- In case of illegitimate API calls (e.g. wrong + value types, missing dictionary keys, etc.) + + + ------- Detailed Description ------- + + CommonWidgetInterface offers two ways of updating / reading + a widget's properties: + + - by passing the Qt object (and value) on to public 'meta' + getter/setter methods like getValue() or getCurrent() + - by using the convenience methods set() and get() which take + the widget name and property name and automatically + determine the right 'meta' getter / setter methods to call + + In the second case, mapping between property names and corresponding + 'meta' getter/setter methods is assigned in the methods_by_key dictionary. + + In both cases, 'meta' getter methods like setValue or getValue + in turn call the correct Qt API getters/setters depending + on the widget type (sometimes after passing through other public + 'meta' getters/setters). + + A list of supported Qt widgets, properties, and data types may be found + below: + + --- Setters --- + + QWidget | value items current min max + ================|===================================================== + QColorButton | str - - - - + QKeyGrabButton | str - - - - + QCheckBox | bool - - - - + QRadioButton | bool - - - - + QSpinBox | numeric - - numeric numeric + QDoubleSpinBox | numeric - - numeric numeric + QSlider | numeric - - numeric numeric + QComboBox | immutable listtypes immutable - - + QListWidget | listtypes listtypes immutable - - + QDateEdit | int - - int int + QLineEdit | str - - - - + QLabel | str - - - - + QPushButton | str - - - - + QTextEdit | str - - - - + QPlainTextEdit | str - - - - + QFontComboBox | dict - - - - + QKeySequenceEdit| str - - - - + + --- Getters --- + + QWidget | value items current min max + ================|===================================================== + QColorButton | str - - - - + QKeyGrabButton | str - - - - + QCheckBox | bool - - - - + QRadioButton | bool - - - - + QSpinBox | numeric - - - - + QDoubleSpinBox | numeric - - - - + QSlider | numeric - - - - + QComboBox | immutable listtypes immutable - - + QListWidget | listtypes listtypes immutable - - + QDateEdit | int - - - - + QLineEdit | str - - - - + QLabel | str - - - - + QPushButton | str - - - - + QTextEdit | str - - - - + QPlainTextEdit | str - - - - + QFontComboBox | dict - - - - + QKeySequenceEdit| str - - - - + + --- Additional notes --- + + QWidget | Notes + ================|===================================================== + QColorButton | str should be an HTML color code (e.g. "#FFFFFF") + ---------------------------------------------------------------------- + QKeyGrabButton | str should be a valid Qt key sequence (e.g. "Ctrl+F") + ---------------------------------------------------------------------- + QComboBox | items should be a tuple/list of tuples of the form + | (item_text {str}, item_data {immutable}) + | value & current are mapped to the data of + | the current item + ---------------------------------------------------------------------- + QListWidget | items should be a tuple/list of tuples of the form + | (item_text, item_data) + | value is mapped to a list of all item data + | current is mapped to the data of the current item + ---------------------------------------------------------------------- + QDateEdit | value should be valid unix time in secs since epoch + ---------------------------------------------------------------------- + QTextEdit, | value can either be plain text or HTML + QLabel | + ---------------------------------------------------------------------- + QFontComboBox | value should be a dictionary with the following keys: + | - "family": font family {str} + | - "size": font point size {int} + | - "bold": font bold state {bool} + | - "italic": font italics state {bool} + + --- Legend --- + + Property keys: + + "value": Corresponds to the most commonly used property of each + widget. E.g. in case of a QComboBox this might be the + current item (or rather, its current data), while for a + QListWidget it may be a list of all items + (or rather, their data). + + "items": Relevant only to widgets with multiple user-modifiable + items. Corresponds to all widget items. + + "current": Relevancy same as above. Corresponds to the current + widget item (or rather, its data) + + "min"/"max": Relevant only to widgets with upper/lower boundaries. + + Property keys are assigned to different 'meta' methods in + the methods_by_key dictionary. + + Types: + + "numeric": int, float + "immutable": e.g. str, int, float + "listtypes": list, tuple + + """ + + # Property names assigned in this dictionary are used by set() / get() + # to determine the correct 'meta' setter/getter to call + # + # Each value should be a tuple of + # (setter_method_name {str}, getter_method_name {str}) + # where each name corresponds to a method of CommonWidgetInterface + # + # In case of an undefined setter or getter None should be used, instead. + methods_by_key = { + "value": ("setValue", "getValue"), + "items": ("setValueList", "getValueList"), + "current": ("setCurrentByData", "getCurrentData"), + "min": ("setMinValue", None), + "max": ("setMaxValue", None), + } + + def __init__(self, parent): + self.parent = parent + + # API + ###################################################################### + + # COMMON + + # Convenience methods that resolve widget_name -> widget and + # property_name -> setter/getter method mappings defined in + # methods_by_key + + def set(self, widget_name, property_name, data): + """ + Sets widget data for given widget name, property name, and data + + Arguments: + widget_name {str} -- Object name of Qt widget found in parent. + Dot-separated attribute names are resolved + automatically (e.g. "form.button" would + be evaluated as self.parent.form.button) + property_name {str} -- Name of the property to update, as defined + in CommonWidgetInterface.methods_by_key. + Currently supported: + value, items, current, min, max + data {obj} -- Data to set widget property to. Has to follow correct + type specs (see class-level docstring) + + Returns: + object -- Setter return value + """ + widget = self.nameToWidget(widget_name) + + try: + setter = getattr(self, self.methods_by_key[property_name][0]) + except KeyError as error: + error.args += ("Unrecognized widget property name: ", + property_name) + raise + except TypeError as error: + error.args += ("Setter not defined for widget property name: ", + property_name) + raise + + return setter(widget, data) + + def get(self, widget_name, property_name): + """ + Gets widget data for given widget name and property name + + Arguments: + widget_name {str} -- Object name of Qt widget. Dot-separated + attribute names are resolved automatically + (e.g. "form.button" would resolve to + self.parent.form.button) + property_name {str} -- Name of the property to update. Currently + supported: value, items, current + + Returns: + object -- Data assigned to widget property. Types follow type specs + defined in class-level docstring. + """ + widget = self.nameToWidget(widget_name) + + try: + getter = getattr(self, self.methods_by_key[property_name][1]) + except KeyError as error: + error.args += ("Unrecognized widget property name: ", + property_name) + raise + except TypeError as error: # raised when method name is None + error.args += ("Setter not defined for widget property name: ", + property_name) + raise + + return getter(widget) + + # Regular interface + + def setValue(self, widget, data): + """ + Sets the current value for the provided widget. + + What constitutes the widget value varies depending on the widget, but + tries to reflect the most common use case of that particular widget. + + For more information on the supported widgets and updated properties + for each widget please see the class-level docstring. + + Arguments: + widget {QWidget} -- Qt widget to update + data {obj} -- Data to set widget property to. Has to follow correct + type specs (see class-level docstring) + + Raises: + NotImplementedError -- In case of an unimplemented widget + AssertionErorr -- In case of illegitimate API calls (e.g. wrong + value types, missing dictionary keys, etc.) + """ + error_msg = "Invalid type {} for widget {}".format(type(data), widget) + + # custom widgets need to be listed first as they usually inherit + # from default Qt widgets + if isinstance(widget, QColorButton): + assert isinstance(data, STRINGTYPES), error_msg + widget.setColor(data) + elif isinstance(widget, QKeyGrabButton): + assert (isinstance(data, STRINGTYPES)), error_msg + widget.setKey(data) + elif isinstance(widget, (QCheckBox, QRadioButton, QGroupBox)): + assert isinstance(data, bool), error_msg + widget.setChecked(data) + elif isinstance(widget, (QSpinBox, QDoubleSpinBox, QSlider)): + assert isinstance(data, NUMERICTYPES), error_msg + widget.setValue(data) + elif isinstance(widget, QComboBox): + # data should be non-mutable + assert not issubclass(type(data), MUTABLES), error_msg + self._setComboCurrentByData(widget, data) + elif isinstance(widget, QListWidget): + try: + self._checkItemTuples(data) + except AssertionError as error: + error.args.append(error_msg) + raise + self._addListValues(widget, data, clear=True) + elif isinstance(widget, QDateEdit): + assert isinstance(data, int), error_msg + self._setDateTime(widget, data) + elif isinstance(widget, (QLineEdit, QLabel, QPushButton)): + assert (isinstance(data, STRINGTYPES)), error_msg + widget.setText(data) + elif isinstance(widget, QTextEdit): + assert (isinstance(data, STRINGTYPES)), error_msg + widget.setHtml(data) + elif isinstance(widget, QPlainTextEdit): + assert (isinstance(data, STRINGTYPES)), error_msg + widget.setPlainText(data) + elif isinstance(widget, QFontComboBox): + assert isinstance(data, dict) + self._setFontComboCurrent(widget, data) + elif isinstance(widget, QKeySequenceEdit): + widget.clear() + widget.setKeySequence(QKeySequence(data)) + else: + raise NotImplementedError( + "setValue not implemented for widget ", widget) + + def getValue(self, widget): + """ + Gets the current value for the provided widget. + + What constitutes the widget value varies depending on the widget, but + tries to reflect the most common use case of that particular widget. + + For more information on the supported widgets and returned + properties for each widget please see the class-level docstring. + + Arguments: + widget {QWidget} -- Qt widget to read data from + + Raises: + NotImplementedError -- In case of an unimplemented widget + AssertionError -- In case of illegitimate API calls (e.g. wrong + value types, missing dictionary keys, etc.) + """ + + # custom widgets need to be listed first as they usually inherit + # from default Qt widgets + if isinstance(widget, QColorButton): + return widget.color() + elif isinstance(widget, QKeyGrabButton): + return widget.key() + elif isinstance(widget, (QCheckBox, QRadioButton, QGroupBox)): + return widget.isChecked() + elif isinstance(widget, (QSpinBox, QDoubleSpinBox, QSlider)): + return widget.value() + elif isinstance(widget, QComboBox): + return self._getComboCurrentData(widget) + elif isinstance(widget, QListWidget): + return self._getListData(widget) + elif isinstance(widget, QDateEdit): + return self._getDateTime(widget) + elif isinstance(widget, (QLineEdit, QLabel, QPushButton)): + return widget.text() + elif isinstance(widget, QTextEdit): + return widget.toHtml() + elif isinstance(widget, QPlainTextEdit): + return widget.toPlainText() + elif isinstance(widget, QFontComboBox): + return self._getFontComboCurrent(widget) + elif isinstance(widget, QKeySequenceEdit): + return widget.keySequence().toString() + else: + raise NotImplementedError( + "getValue not implemented for widget ", widget) + + # WIDGETS WITH MULTIPLE ITEMS TO CHOOSE FROM: + + # setter + + def setValueList(self, widget, values, current=None, clear=True): + """ + Sets the items of multi-item widgets based on a list of + provided values. + + For more information on the supported widgets and updated properties + for each widget please see the class-level docstring. + + Arguments: + widget {QWidget} -- Qt widget to update. Supported: + QComboBox, QListWidget + values {list,tuple} -- Sequence of values to create widget items + from. Each value in the sequence should be + a tuple of the form: (item_text, item_data) + + Keyword Arguments: + current {immutable} -- Item to set as the current widget item, + as characterized by its data + (default: {None}) + clear {bool} -- Whether to clear all existing widget items before + creating any new items (default: {True}) + + Raises: + NotImplementedError -- In case of an unimplemented widget + AssertionError -- In case of illegitimate API calls (e.g. wrong + value types, missing dictionary keys, etc.) + + Returns: + object -- Setter return value + """ + try: + self._checkItemTuples(values) + assert not issubclass(type(current), MUTABLES), \ + "current data should be an immutable type (e.g. str or int)" + assert isinstance(clear, bool), \ + "clear should be set to a boolean" + except AssertionError as error: + error.args += ("Widget: ", widget) + raise + + if isinstance(widget, QComboBox): + return self._addComboValues(widget, values, current_data=current, + clear=clear) + elif isinstance(widget, QListWidget): + return self._addListValues(widget, values, current_data=current, + clear=clear) + else: + raise NotImplementedError( + "setValues not implemented for widget ", widget) + + def setValueListAndCurrent(self, widget, values, current): + """ + Convenience method to set a series of widget items and select + a specific item to be the current item. + + See setValueList docstring for the method signature. + + Type checking and error handling delegated to setValueList + """ + return self.setValueList(widget, values, current=current) + + def addValues(self, widget, values): + """ + Convenience method to add a series of widget items without + removing the existing ones. + + See setValueList docstring for the method signature. + + Type checking and error handling delegated to setValueList + """ + return self.setValueList(widget, values, clear=False) + + def addValueAndMakeCurrent(self, widget, value): + """ + Convenience method to add a widget item and make it the current one. + + See setValueList docstring for the method signature. + + Type checking and error handling delegated to setValueList + """ + return self.setValueList(widget, [value], current=value[1], + clear=False) + + def removeItemsByData(self, widget, data_to_remove): + """ + Removes items from a widget by the provided sequence of data values + + Arguments: + widget {QWidget} -- Qt widget to update. Supported: + QComboBox, QListWidget + data_to_remove {list,tuple} -- Sequence of data values to identify + and remove items by. Values should + be immutable types (e.g. str or int) + + Raises: + NotImplementedError -- In case of an unimplemented widget + AssertionError -- In case of illegitimate API calls (e.g. wrong + value types, missing dictionary keys, etc.) + """ + assert isinstance(data_to_remove, LISTTYPES), \ + "data_to_remove should be a list or tuple" + assert (not data_to_remove or + not issubclass(type(data_to_remove[0]), MUTABLES)), \ + "data_to_remove should contain immutables (e.g. str or int)" + + if isinstance(widget, QComboBox): + return self._removeComboItemsByData(widget, data_to_remove) + elif isinstance(widget, QListWidget): + return self._removeListItemsByData(widget, data_to_remove) + else: + raise NotImplementedError( + "removeValues not implemented for widget ", widget) + + def removeSelected(self, widget): + """ + Removes currently selected item(s) of a widget + + Arguments: + widget {QWidget} -- Qt widget to update. Supported: + QListWidget + + Raises: + NotImplementedError -- In case of an unimplemented widget + """ + if isinstance(widget, QListWidget): + selected = self.getSelected(widget) + for item in selected: + self._removeListItem(widget, item) + else: + raise NotImplementedError( + "removeSelectedValues not implemented for widget ", widget) + + def setCurrentByData(self, widget, data_current): + """ + Set the current widget item by the provided widget data + + Arguments: + widget {Qt widget} -- Qt widget to update. Supported: + QComboBox, QListWidget + data_current {immutable} -- Data to identify current item by + + Raises: + NotImplementedError -- In case of an unimplemented widget + AssertionError -- In case of illegitimate API calls (e.g. wrong + value types, missing dictionary keys, etc.) + + Returns: + bool -- True if item found + """ + assert not issubclass(type(data_current), MUTABLES), \ + "data_current should be an immutable object (e.g. str or int)" + + if isinstance(widget, QListWidget): + return self._setListCurrentByData(widget, data_current) + elif isinstance(widget, QComboBox): + return self._setComboCurrentByData(widget, data_current) + else: + raise NotImplementedError( + "setCurrent not implemented for widget ", widget) + + # getter + + def getValueList(self, widget): + """ + Get list of current widget values + + Arguments: + widget {QWidget} -- Qt widget to read. Supported: + QComboBox, QListWidget + + Raises: + NotImplementedError -- In case of an unimplemented widget + + Returns: + list -- List of tuples of the form (item_text, item_data) + """ + if isinstance(widget, QComboBox): + return self._getComboValues(widget) + elif isinstance(widget, QListWidget): + return self._getListValues(widget) + else: + raise NotImplementedError( + "getValues not implemented for widget ", widget) + + def getCurrentData(self, widget): + """ + Get list of current widget data properties + + Arguments: + widget {QWidget} -- Qt widget to read. Supported: + QComboBox, QListWidget + + Raises: + NotImplementedError -- In case of an unimplemented widget + + Returns: + list -- List of data properties (immutables, e.g. str or int) + """ + if isinstance(widget, QComboBox): + return self._getComboCurrentData(widget) + elif isinstance(widget, QListWidget): + return self._getListCurrentData(widget) + else: + raise NotImplementedError( + "getCurrent not implemented for widget ", widget) + + def getSelected(self, widget): + """ + Get list of selected widget items + + Arguments: + widget {QWidget} -- Qt widget to read. Supported: + QListWidget + + Raises: + NotImplementedError -- In case of an unimplemented widget + + Returns: + list -- List of QWidgets corresponding to the current widget items + """ + if isinstance(widget, QListWidget): + return widget.selectedItems() + else: + raise NotImplementedError( + "getSelected not implemented for widget ", widget) + + # WIDGETS WITH VALUE BOUNDARIES: + + def setMinValue(self, widget, value): + """ + Set lower boundary of widget + + Arguments: + widget {Qt widget} -- Qt widget to update. Supported: + QSpinBox, QDoubleSpinBox, QDateEdit + value {int,float} -- Number to set lower boundary to. + In case of QDateEdit: + - value should be valid unix time in secs + since epoch + + Raises: + NotImplementedError -- In case of an unimplemented widget + + Returns: + object -- Setter return value + """ + try: + assert isinstance(value, (int, float)), \ + "value should be an int or float" + except AssertionError as error: + error.args += ("Widget: ", widget) + raise + + if isinstance(widget, (QSpinBox, QDoubleSpinBox, QSlider)): + return widget.setMinimum(value) + elif isinstance(widget, QDateEdit): + return self._setDateTimeMin(widget, value) + else: + raise NotImplementedError( + "setMinValue not implemented for widget ", widget) + + def setMaxValue(self, widget, value): + """ + Set upper boundary of widget + + Arguments: + widget {Qt widget} -- Qt widget to update. Supported: + QSpinBox, QDoubleSpinBox, QDateEdit + value {int,float} -- Number to set upper boundary to. + In case of QDateEdit: + - value should be valid unix time in secs + since epoch + + Raises: + NotImplementedError -- In case of an unimplemented widget + + Returns: + object -- Setter return value + """ + try: + assert isinstance(value, (int, float)), \ + "value should be an int or float" + except AssertionError as error: + error.args += ("Widget: ", widget) + raise + + if isinstance(widget, (QSpinBox, QDoubleSpinBox, QSlider)): + return widget.setMaximum(value) + elif isinstance(widget, QDateEdit): + return self._setDateTimeMax(widget, value) + else: + raise NotImplementedError( + "setMaxValue not implemented for widget ", widget) + + # UTILITY + + def nameToWidget(self, name): + """ + Gets widget corresponding to attribute name. + + Dot-separated attribute names are resolved automatically + (e.g. "form.button" would resolve to self.parent.form.button) + + Arguments: + name {str} -- Object name of widget + + Raises: + NotImplementedError -- Should the widget not be found + + Returns: + QWidget -- widget corresponding to attribute name + """ + assert isinstance(name, STRINGTYPES), \ + "name should be a string type" + try: + return getNestedAttribute(self.parent, name) + except AttributeError: + raise NotImplementedError( + "Widget not implemented: ", name) + + # PRIVATE + ###################################################################### + + # COMMON + + def _selectWidgetItem(self, widget, item): + """ + Select widget item + """ + widget.selectionModel().clearSelection() + widget.setCurrentItem(item) + item.setSelected(True) + + def _getWidgetItems(self, widget): + """ + Get list of current widget items + """ + return [widget.item(idx) for idx in range(widget.count())] + + def _checkItemTuples(self, values): + """ + Check validity of item tuples + """ + # values provided as tuples of (str: label, immutable: data) + assert isinstance(values, LISTTYPES), \ + "values should be provided as a list/tuple of tuples" + # lazy check for first tuple. should catch most type errors: + assert(len(values) == 0 or + (len(values[0]) == 2 and + isinstance(values[0][0], STRINGTYPES) and + not issubclass(type(values[0][1]), MUTABLES)) + ), \ + "expected tuple types: (str: label, immutable (e.g. str): data" + return True + + # WIDGET-SPECIFIC METHODS + + # QDateTimeEdit (dateedit) + + # setter + + def _createDateTimeFromUnix(self, unixtime): + """ + Create QDateTime object and set it to unix time in secs + """ + qdatetime = QDateTime() + qdatetime.setTime_t(unixtime) + return qdatetime + + def _setDateTime(self, qdatetimeedit, curtime): + """ + Update date & time of QDateTimeEdit from unix time in secs + """ + return qdatetimeedit.setDateTime(self._createDateTimeFromUnix(curtime)) + + def _setDateTimeMin(self, qdatetimeedit, mintime): + """ + Update min date & time of QDateTimeEdit from unix time in secs + """ + return qdatetimeedit.setMinimumDateTime( + self._createDateTimeFromUnix(mintime)) + + def _setDateTimeMax(self, qdatetimeedit, maxtime): + """ + Update max date & time of QDateTimeEdit from unix time in secs + """ + return qdatetimeedit.setMaximumDateTime( + self._createDateTimeFromUnix(maxtime)) + + # getter + + def _getDateTime(self, qdatetimeedit): + """ + Get current unix time from QDateTimeEdit + """ + qdatetime = qdatetimeedit.dateTime() + # Qt4 does not support toSecsSinceEpoch + timestamp = int(round(qdatetime.toMSecsSinceEpoch() / 1000)) + return timestamp + + # QComboBox + + # setter + + def _addComboValues(self, combo_widget, item_tuples, + current_data=None, clear=False): + """ + Add combo items by list of (item_text, item_data) tuples + """ + if clear: + combo_widget.clear() + + idx = 0 + cur_idx = None + for text, data in item_tuples: + combo_widget.addItem(text, data) + if current_data is not None and data == current_data: + cur_idx = idx + idx += 1 + + if cur_idx: + combo_widget.setCurrentIndex(cur_idx) + + def _removeComboValues(self, combo_widget, item_tuples): + """ + Remove items by list of (item_text, item_data) tuples + """ + return self._removeComboItemsByData([item[1] for item in item_tuples]) + + def _removeComboItemsByData(self, combo_widget, data_to_remove): + """ + Remove items by list of item_data + """ + for idx in range(combo_widget.count()): + data = combo_widget.itemData(idx, Qt.UserRole) + if data in data_to_remove: + self._removeComboItemByIndex(idx) + + def _removeComboItemByIndex(self, combo_widget, index): + """ + Remove item by model index (int) + """ + return combo_widget.removeItem(index) + + def _setComboCurrentIndex(self, combo_widget, index): + """ + Set current item by model index (int) + """ + return combo_widget.setCurrentIndex(index) + + def _setComboCurrentByData(self, combo_widget, item_data): + """ + Set current item by item_dta + """ + index = combo_widget.findData(item_data) + if index == -1: # not found + return False + self._setComboCurrentIndex(combo_widget, index) + return True + + # getter + + def _getComboValues(self, combo_widget): + """ + Get list of current (item_text, item_data) tuples + """ + result_list = [] + for idx in range(combo_widget.count()): + text = combo_widget.itemText(idx) + data = combo_widget.itemData(idx, Qt.UserRole) + result_list.append((text, data)) + return result_list + + def _getComboData(self, combo_widget): + """ + Get list of item_data values + """ + return [item[1] for item in self._getComboValues(combo_widget)] + + def _getComboCurrentIndex(self, combo_widget): + """ + Get model index of current item (int) + """ + return combo_widget.currentIndex() + + def _getComboCurrentValue(self, combo_widget): + """ + Get current (item_text, item_data) tuple + """ + index = self._getComboCurrentIndex(combo_widget) + text = combo_widget.currentText() + data = combo_widget.itemData(index, Qt.UserRole) + return (text, data) + + def _getComboCurrentData(self, combo_widget): + """ + Get item_data of current item + """ + return self._getComboCurrentValue(combo_widget)[1] + + # QListWidget + + # setter + + def _addListValues(self, list_widget, item_tuples, + current_data=None, clear=False): + """ + Add list items by list of (item_text, item_data) tuples + """ + if clear: + list_widget.clear() + + for text, data in item_tuples: + new_item = QListWidgetItem(text) + if data: + new_item.setData(Qt.UserRole, data) + list_widget.addItem(new_item) + if current_data is not None and data == current_data: + self._selectWidgetItem(list_widget, new_item) + + def _removeListValues(self, list_widget, item_tuples): + """ + Remove items by list of (item_text, item_data) tuples + """ + return self._removeListItemsByData([item[1] for item in item_tuples]) + + def _removeListItemsByData(self, list_widget, data_to_remove): + """ + Remove items by list of item_data + """ + for idx in range(list_widget.count()): + item = list_widget.item(idx) + data = item.data(Qt.UserRole) + if data in data_to_remove: + self._removeListItem(list_widget, item) + + def _removeListItem(self, list_widget, item): + """ + Remove QListWidgetItem from list widget + """ + list_widget.takeItem(list_widget.row(item)) + # takeItem does not delete the QListWidgetItem: + del(item) + + def _setListCurrentByData(self, list_widget, item_data): + """ + Set current item by item_dta + """ + for item in self._getWidgetItems(list_widget): + data = item.data(Qt.UserRole) + if data == item_data: + self._selectWidgetItem(list_widget, item) + return True + return False + + # getter + + def _getListValues(self, list_widget): + """ + Get list of current (item_text, item_data) tuples + """ + result_list = [] + for item in self._getWidgetItems(list_widget): + data = item.data(Qt.UserRole) + text = item.text() + result_list.append((text, data)) + return result_list + + def _getListData(self, list_widget): + """ + Get list of item_data values + """ + return [item[1] for item in self._getListValues(list_widget)] + + def _getListCurrentIndex(self, list_widget): + """ + Get model index of current item {int} + """ + return list_widget.currentRow() + + def _getListCurrentItem(self, list_widget): + """ + Get current QListWidgetItem + """ + return list_widget.currentItem() + + def _getListCurrentValue(self, list_widget): + """ + Get current (item_text, item_data) tuple + """ + item = self._getListCurrentItem(list_widget) + text = item.text() + data = item.data(Qt.UserRole) + return (text, data) + + def _getListCurrentData(self, list_widget): + """ + Get item_data of current item + """ + return self._getListCurrentValue()[1] + + # QFontComboBox + + # setter + + def _setFontComboCurrent(self, font_widget, font_dict): + """ + Set font combo state from dictionary of font properties + + Arguments: + font_widget {QFontComboBox} -- Font combo box to update + font_dict {dict} -- Dictionary of font properties. Keys: + family {str} -- Font family [required] + size {int} -- Font size in pt [optional] + bold {bool} -- Bold state [optional] + italic {bool} -- Italics state [optional] + + """ + family = font_dict.get("family", None) + size = font_dict.get("size", None) + bold = font.setBold(font_dict["bold"]) + italic = font_dict.get("italic", None) + + assert family is not None and isinstance(family, STRINGTYPES), \ + "font family needs to be provided as a string type" + font = Qfont(font_dict["family"]) + + if size is not None: + assert isinstance(size, (int, float)) + font.setPointSize(size) + if bold is not None: + assert isinstance(bold, bool) + font.setBold(bold) + if italic is not None: + assert isinstance(italic, bool) + font.setItalic(italic) + + return font_widget.setCurrentFont(font) + + # getter + + def _getFontComboCurrent(self, font_widget): + """ + Set font combo state as dictionary of font properties + + Arguments: + font_widget {QFontComboBox} -- Font combo box to update + + Returns: + dict -- Dictionary of font properties. Keys: + family {str} -- Font family [required] + size {int} -- Font size in pt [optional] + bold {bool} -- Bold state [optional] + italic {bool} -- Italics state [optional] + """ + font_dict = { + "family": font_widget.family(), + "size": font_widget.pointSize(), + "bold": font_widget.bold(), + "italic": font_widget.italic() + } + return font_dict diff --git a/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/__init__.py b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/__init__.py new file mode 100644 index 00000000..2d8702ab --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Package for custom reusable Qt widgets +""" diff --git a/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qcolorbutton.py b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qcolorbutton.py new file mode 100644 index 00000000..23343c38 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qcolorbutton.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Custom color-chooser +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from .qt import QPushButton, QColorDialog, QPixmap, QColor, QIcon, QSize + +class QColorButton(QPushButton): + def __init__(self, parent=None, color="#000000"): + super(QColorButton, self).__init__(parent=parent) + self._updateButtonColor(color) + self.clicked.connect(self._chooseColor) + + def _chooseColor(self): + qcolour = QColor(self.color) + dialog = QColorDialog(qcolour, parent=self) + color = dialog.getColor() + if not color.isValid(): + return False + color = color.name() + self._updateButtonColor(color) + + def _updateButtonColor(self, color): + """Generate color preview pixmap and place it on button""" + pixmap = QPixmap(128, 18) + qcolour = QColor(0, 0, 0) + qcolour.setNamedColor(color) + pixmap.fill(qcolour) + self.setIcon(QIcon(pixmap)) + self.setIconSize(QSize(128, 18)) + self.color = color + + def color(self): + return self.color + + def setColor(self, color): + self._updateButtonColor(color) diff --git a/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qkeygrabber.py b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qkeygrabber.py new file mode 100644 index 00000000..fd9ef6bc --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qkeygrabber.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Custom hotkey selector + +NOTE: obsolete on PyQt5 +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from ....platform import PLATFORM + +from .qt import QDialog, QPushButton, QVBoxLayout, QLabel, Qt, QKeySequence + +PLATFORM_MODKEY_NAMES = { + "lin": {"meta": "Meta", "ctrl": "Ctrl", + "alt": "Alt", "shift": "Shift"}, + "win": {"meta": "Win", "ctrl": "Ctrl", "alt": + "Alt", "shift": "Shift"}, + "mac": {"meta": "Control", "ctrl": "Command", + "alt": "Option", "shift": "Shift"} +} + +class QKeyGrabButton(QPushButton): + def __init__(self, parent=None, key_string=""): + super(QKeyGrabButton, self).__init__("", parent=parent) + self.setKey(key_string) + self.clicked.connect(self.grabKey) + + def setKey(self, key_string): + self.key_string = key_string + qkeyseq = QKeySequence(key_string, QKeySequence.PortableText) + native_key_string = qkeyseq.toString(format=QKeySequence.NativeText) + self.setText(native_key_string) + + def key(self): + return self.key_string + + def grabKey(self): + """Invoke key grabber""" + grabber = QKeyGrab(self.parent()) + ret = grabber.exec_() + if ret != 1: + return + key_string = grabber.key_string + if not key_string: # or not ret + return + self.setKey(key_string) + + +class QKeyGrab(QDialog): + """ + Simple key combination grabber for hotkey assignments + + Based in part on ImageResizer by searene + (https://github.com/searene/Anki-Addons) + """ + + modkey_names = PLATFORM_MODKEY_NAMES[PLATFORM] + + def __init__(self, parent): + """ + Initialize dialog + + Arguments: + parent {QWidget} -- Parent Qt widget + """ + QDialog.__init__(self, parent=parent) + self.parent = parent + # self.active is used to trace whether there's any key held now: + self.active = 0 + self._resetDialog() + self._setupUI() + + def _setupUI(self): + """Basic UI setup""" + mainLayout = QVBoxLayout() + self.label = QLabel("Please press the key combination\n" + "you would like to assign") + self.label.setAlignment(Qt.AlignCenter) + mainLayout.addWidget(self.label) + self.setLayout(mainLayout) + self.setWindowTitle("Grab key combination") + + def _resetDialog(self): + self.extra = self.key_string = None + self.meta = self.ctrl = self.alt = self.shift = False + + def keyPressEvent(self, evt): + """ + Intercept key presses and save current key plus + active modifiers. + + Arguments: + evt {QKeyEvent} -- Intercepted key press event + """ + self.active += 1 + + key = evt.key() + if key > 0 and key < 127: + self.extra = chr(key) + elif key == Qt.Key_Control: + self.ctrl = True + elif key == Qt.Key_Alt: + self.alt = True + elif key == Qt.Key_Shift: + self.shift = True + elif key == Qt.Key_Meta: + self.meta = True + else: + self.extra = QKeySequence(key).toString() + self.other = True + + def keyReleaseEvent(self, evt): + """ + Intercept key release event, checking and then saving key combo + and exiting dialog. + + Arguments: + evt {QKeyEvent} -- Intercepted key release event + """ + self.active -= 1 + + if self.active != 0: + # at least 1 key still held + return + + # TODO: platform-specific messages + msg = None + if not (self.shift or self.ctrl or self.alt or self.meta or self.other): + msg = ("Please use at least one keyboard modifier\n" + "({meta}, {ctrl}, {alt}, {shift})".format( + **self.modkey_names)) + if (self.shift and not (self.ctrl or self.alt or self.meta or self.other)): + msg = ("Shift needs to be combined with at least one\n" + "other modifier ({meta}, {ctrl}, {alt})".format( + **self.modkey_names)) + if not self.extra: + msg = ("Please press at least one key that is \n" + "not a modifier (not {meta}, {ctrl}, " + "{alt}, or {shift})".format( + **self.modkey_names)) + + if msg: + self.label.setText(msg) + self._resetDialog() + return + + combo = [] + if self.meta: + combo.append("Meta") + if self.ctrl: + combo.append("Ctrl") + if self.shift: + combo.append("Shift") + if self.alt: + combo.append("Alt") + combo.append(self.extra) + + self.key_string = "+".join(combo) + + self.accept() diff --git a/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qt.py b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qt.py new file mode 100644 index 00000000..afeb3b3a --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qt.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018 Aristotelis P. +# Copyright (C) 2013-2018 Damien Elmes +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Qt imports +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +# extracted from aqt.qt: +import sip + +try: + from PyQt5.Qt import * # noqa: F401 +except ImportError: + sip.setapi('QString', 2) + sip.setapi('QVariant', 2) + sip.setapi('QUrl', 2) + try: + sip.setdestroyonexit(False) + except: # noqa: E722 + # missing in older versions + pass + from PyQt4.QtCore import * # noqa: F401 + from PyQt4.QtGui import * # noqa: F401 diff --git a/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qutils.py b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qutils.py new file mode 100644 index 00000000..7dedabbb --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/basic/widgets/qutils.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Miscellaneous Qt utilities +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from .qt import QMessageBox + +def showInfo(message, parent=None, mode="info", title="Anki"): + if mode == "info": + icon = QMessageBox.Information + elif mode == "warning": + icon = QMessageBox.Warning + elif mode == "critical": + icon = QMessageBox.Critical + + return QMessageBox(icon, title, message, parent=parent) diff --git a/src/image_occlusion_enhanced/libaddon/gui/dialog_configeditor.py b/src/image_occlusion_enhanced/libaddon/gui/dialog_configeditor.py new file mode 100644 index 00000000..5adabb83 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/dialog_configeditor.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +anki20 compat: Add-on configuration editor +""" + +import aqt +from aqt.qt import * +from aqt.utils import tooltip, showInfo + +from anki.utils import json + +from .._vendor import markdown2 + +from ..consts import ADDON +from ..platform import PATH_THIS_ADDON + +from .dialog_htmlview import HTMLViewer + + +class ConfigEditor(QDialog): + + def __init__(self, config_manager, parent): + super(ConfigEditor, self).__init__(parent=parent) + self.mgr = config_manager + self.form = aqt.forms.editaddon.Ui_Dialog() + self.form.setupUi(self) + self.setWindowTitle("{} Configuration".format(ADDON.NAME)) + self.setupWidgets() + self.updateText(self.mgr["local"]) + self.exec_() + + def setupWidgets(self): + button_box = self.form.buttonBox + restore_btn = button_box.addButton(QDialogButtonBox.RestoreDefaults) + help_btn = button_box.addButton(QDialogButtonBox.Help) + help_btn.clicked.connect(self.onHelpRequested) + restore_btn.clicked.connect(self.onRestoreDefaults) + + def updateText(self, conf): + self.form.text.setPlainText( + json.dumps(conf, ensure_ascii=False, sort_keys=True, + indent=4, separators=(',', ': '))) + + def onRestoreDefaults(self): + default_conf = self.mgr.defaults["local"] + self.updateText(default_conf) + tooltip("Restored defaults", parent=self) + + def onHelpRequested(self): + docs_path = os.path.join(PATH_THIS_ADDON, "config.md") + if not os.path.exists(docs_path): + return False + with open(docs_path, "r") as f: + html = markdown2.markdown(f.read()) + dialog = HTMLViewer(html, title="{} Configuration Help".format( + ADDON.NAME), parent=self) + dialog.show() + + def accept(self): + txt = self.form.text.toPlainText() + error = None + try: + new_conf = json.loads(txt) + except ValueError as e: + new_conf = None + error = repr(e) + + if new_conf and not isinstance(new_conf, dict): + error = "Top level object must be a dictionary." + + if error: + showInfo("The configuration seems to be invalid. Please make " + "sure you haven't made a typo or forgot a control " + "character (e.g. commas, brackets, etc.). " + "Original error message follows below:\n\n{}" + "\n\nIf you're not sure what's wrong you can start " + "from scratch by clicking on 'Restore Defaults' " + "in the config window.".format(error)) + return + + act = self.mgr.conf_updated_action + if act: + act(new_conf) + + super(ConfigEditor, self).accept() diff --git a/src/image_occlusion_enhanced/libaddon/gui/dialog_contrib.py b/src/image_occlusion_enhanced/libaddon/gui/dialog_contrib.py new file mode 100644 index 00000000..d49c76ad --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/dialog_contrib.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Contributions diaog + +Uses the following addon-level constants, if defined: + +ADDON.NAME, ADDON.AUTHOR_MAIL, ADDON.LINKS +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from aqt.utils import openLink + +from ..consts import ADDON + +from .basic.dialog_basic import BasicDialog +from .labelformatter import formatLabels + +from .dialog_htmlview import HTMLViewer +from .about import getAboutString + + +class ContribDialog(BasicDialog): + """ + Add-on agnostic dialog that presents user with a number + of options to support the development of the add-on. + """ + + def __init__(self, form_module, parent=None): + """ + Initialize contrib dialog with provided form + + Arguments: + form_module {PyQt form module} -- PyQt dialog form outlining the UI + + Provided Qt form should contain the following widgets: + QPushButton: btnMail, btnCoffee, btnPatreon, btnCredits + + Keyword Arguments: + parent {QWidget} -- Parent Qt widget (default: {None}) + """ + + super(ContribDialog, self).__init__(form_module=form_module, + parent=parent) + + def _setupUI(self): + formatLabels(self, self._linkHandler) + + def _setupEvents(self): + """ + Connect button presses to actions + """ + mail_string = "mailto:{}".format(ADDON.AUTHOR_MAIL) + self.form.btnMail.clicked.connect( + lambda: openLink(mail_string)) + self.form.btnCoffee.clicked.connect( + lambda: openLink(ADDON.LINKS["coffee"])) + self.form.btnPatreon.clicked.connect( + lambda: openLink(ADDON.LINKS["patreon"])) + self.form.btnCredits.clicked.connect( + self._showCredits) + + def _showCredits(self): + viewer = HTMLViewer(getAboutString(title=True), + title=ADDON.NAME, parent=self) + viewer.exec_() + + def _linkHandler(self, url): + """Support for binding custom actions to text links""" + if not url.startswith("action://"): + return openLink(url) + protocol, cmd = url.split("://") + if cmd == "installed-addons": + print("invoking installed addons dialog") diff --git a/src/image_occlusion_enhanced/libaddon/gui/dialog_htmlview.py b/src/image_occlusion_enhanced/libaddon/gui/dialog_htmlview.py new file mode 100644 index 00000000..a3b3df3e --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/dialog_htmlview.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Simple dialog for viewing HTML +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from aqt.qt import * + +from ..platform import PLATFORM + +from .basic.dialog_basic import BasicDialog + + +class HTMLViewer(BasicDialog): + + def __init__(self, html, title=None, parent=None): + super(HTMLViewer, self).__init__(parent=parent) + if PLATFORM == "win": + self.setMinimumWidth(400) + self.setMinimumHeight(500) + else: + self.setMinimumWidth(500) + self.setMinimumHeight(600) + if title: + self.setWindowTitle(title) + self.setHtml(html) + + def _setupUI(self): + layout = QVBoxLayout(self) + self.setLayout(layout) + self._browser = QTextBrowser(self) + self._browser.setOpenExternalLinks(True) + layout.addWidget(self._browser) + + def setHtml(self, html): + self._browser.setHtml(html) diff --git a/src/image_occlusion_enhanced/libaddon/gui/dialog_options.py b/src/image_occlusion_enhanced/libaddon/gui/dialog_options.py new file mode 100644 index 00000000..573bbfba --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/dialog_options.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Main options dialog +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from aqt.qt import Qt, QUrl, QApplication + +from aqt.utils import openLink, tooltip + +from ..consts import ADDON +from ..platform import PLATFORM +from ..debug import (toggleDebugging, isDebuggingOn, + getLatestLog, openLog, clearLog) + +from .basic.dialog_mapped import MappedDialog +from .about import getAboutString +from .labelformatter import formatLabels + + +class OptionsDialog(MappedDialog): + + def __init__(self, mapped_widgets, config, form_module=None, + parent=None, **kwargs): + """ + Creates an options dialog with the provided Qt form and populates its + widgets from a ConfigManager config object. + + Arguments: + mapped_widgets {sequence} -- A list or tuple of mappings between + widget names, config value names, and + special methods to act as mediators + (see MappedDialog docstring for specs) + config {ConfigManager} -- ConfigManager object providing access to + add-on config values + + Keyword Arguments: + form_module {PyQt form module} -- Dialog form module generated + through pyuic (default: {None}) + parent {QWidget} -- Parent Qt widget (default: {None}) + + + """ + # Mediator methods defined in mapped_widgets might need access to + # certain instance attributes. As super().__init__ instantiates + # all widget values it is important that we set these attributes + # beforehand: + self.config = config + super(OptionsDialog, self).__init__( + mapped_widgets, self.config.all, self.config.defaults, + form_module=form_module, parent=parent) + # Instance methods that modify the initialized UI should either be + # called from self._setupUI or from here + + # Static widget setup + + def _setupUI(self): + formatLabels(self, self._linkHandler) + self._setupAbout() + self._setupLabDebug() + + if PLATFORM == "mac": + # Decrease tab margins on macOS + tab_widget = getattr(self.form, "tabWidget", None) + if not tab_widget: + return + for idx in range(tab_widget.count()): + tab = tab_widget.widget(idx) + if not tab: + continue + layout = tab.layout() + if not layout: + continue + layout.setContentsMargins(3, 3, 3, 3) + + def _setupAbout(self): + """ + Fill out 'about' widget + """ + if hasattr(self.form, "htmlAbout"): + about_string = getAboutString(showDebug=True) + self.form.htmlAbout.setHtml(about_string) + self.form.htmlAbout.setOpenLinks(False) + self.form.htmlAbout.anchorClicked.connect(self._linkHandler) + + def _setupLabDebug(self): + label = getattr(self.form, "labDebug", None) + if not label: + return + if isDebuggingOn(): + label.setText( + "DEBUG ACTIVE") + else: + label.setText("") + + # Events + + def keyPressEvent(self, evt): + """ + Prevent accidentally closing dialog when editing complex widgets + by ignoring Return and Escape + """ + if evt.key() == Qt.Key_Enter or evt.key() == Qt.Key_Return: + return evt.accept() + super(OptionsDialog, self).keyPressEvent(evt) + + def _setupEvents(self): + super(OptionsDialog, self)._setupEvents() + for name, link in ADDON.LINKS.items(): + btn_widget = getattr(self.form, "btn" + name.capitalize(), None) + if not btn_widget: + continue + btn_widget.clicked.connect(lambda _, link=link: openLink(link)) + + # Link actions + + def _linkHandler(self, url): + """Support for binding custom actions to text links""" + if isinstance(url, QUrl): + url = url.toString() + if not url.startswith("action://"): + return openLink(url) + protocol, cmd = url.split("://") + if cmd == "debug-toggle": + self._toggleDebugging() + elif cmd == "debug-open": + self._openDebuglog() + elif cmd == "debug-copy": + self._copyDebuglog() + elif cmd == "debug-clear": + self._clearDebuglog() + elif cmd == "changelog": + self._openChangelog() + + def _toggleDebugging(self): + if toggleDebugging(): + msg = "enabled" + else: + msg = "disabled" + tooltip("Debugging {msg}".format(msg=msg)) + self._setupLabDebug() + + def _copyDebuglog(self): + log = getLatestLog() + if log is False: + tooltip("No debug log has been recorded, yet") + return False + QApplication.clipboard().setText(log) + tooltip("Copied to clipboard") + + def _openDebuglog(self): + ret = openLog() + if ret is False: + tooltip("No debug log has been recorded, yet") + return False + + def _openChangelog(self): + changelog = ADDON.LINKS.get("changelog") + if not changelog: + return + openLink(changelog) + + def _clearDebuglog(self): + ret = clearLog() + if ret is False: + tooltip("No debug log has been recorded, yet") + return False + tooltip("Debug log cleared") + + # Exit handling + + def _onAccept(self): + """Executed only if dialog confirmed""" + self.getData() # updates self.config in place + self.config.save() + super(OptionsDialog, self)._onAccept() diff --git a/src/image_occlusion_enhanced/libaddon/gui/labelformatter.py b/src/image_occlusion_enhanced/libaddon/gui/labelformatter.py new file mode 100644 index 00000000..d6143157 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/gui/labelformatter.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Utilities to fill out predefined data in dialog text labels +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from aqt.qt import * + +from ..consts import ADDON +from ..platform import ANKI20 + +format_dict = { + "ADDON_NAME": ADDON.NAME, + "ADDON_VERSION": ADDON.VERSION, +} + +if not ANKI20: + fmt_find_params = ((QLabel, QPushButton), QRegExp(".*"), + Qt.FindChildrenRecursively) +else: + # Qt4: recursive by default. No third param. + fmt_find_params = ((QLabel, QPushButton), QRegExp(".*")) + + +def formatLabels(dialog, linkhandler=None): + for widget in dialog.findChildren(*fmt_find_params): + if widget.objectName().startswith("fmt"): + widget.setText(widget.text().format(**format_dict)) + if linkhandler and isinstance(widget, QLabel): + widget.linkActivated.connect(linkhandler) diff --git a/src/image_occlusion_enhanced/libaddon/packaging.py b/src/image_occlusion_enhanced/libaddon/packaging.py new file mode 100644 index 00000000..53116245 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/packaging.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# Copyright (C) 2016 Jason R Coombs and other PyPA contributors +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Components related to packaging third-party code and libraries +with Anki add-ons +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +import sys +import os + +from .platform import ANKI20 + +__all__ = [ + "VersionSpecificImporter", + "addPathToModuleLookup", + "addSubdirPathToModuleLookup" +] + +# Resolving version-specific module imports +###################################################################### + +class VersionSpecificImporter: + """ + A PEP 302 meta path importer for finding the right vendored package + among bundled packages specific to Anki 2.1, 2.0, and packages common + to both. + + Presupposes the following package structure: + + root_name + - anki21 + - anki20 + - common + + Where either anki21, anki20, or common contain the packages/modules + supplied in managed_imports. + + vendor_pkg may optionally be supplied in case the vendored packages are + located under a different namespace than root_name. + """ + + module_dir = "anki20" if ANKI20 else "anki21" + + def __init__(self, root_name, managed_imports=(), vendor_pkg=None): + self.root_name = root_name + self.managed_imports = set(managed_imports) + self.vendor_pkg = vendor_pkg or self.root_name + + @property + def search_path(self): + """ + Search version-specific vendor package, then common vendor package, + then global package. + """ + yield ".".join((self.vendor_pkg, self.module_dir, "")) + yield ".".join((self.vendor_pkg, "common", "")) + yield '' + + def find_module(self, fullname, path=None): + """ + Return self when fullname starts with root_name and the + target module is one vendored through this importer. + """ + root, base, target = fullname.partition(self.root_name + '.') + if root: + return + if not any(map(target.startswith, self.managed_imports)): + return + return self + + def load_module(self, fullname): + """ + Iterate over the search path to locate and load fullname. + """ + root, base, target = fullname.partition(self.root_name + '.') + for prefix in self.search_path: + try: + extant = prefix + target + __import__(extant) + mod = sys.modules[extant] + sys.modules[fullname] = mod + # mysterious hack: + # Remove the reference to the extant package/module + # on later Python versions to cause relative imports + # in the vendor package to resolve the same modules + # as those going through this importer. + if sys.version_info >= (3, ): + del sys.modules[extant] + return mod + except ImportError: + pass + else: + raise ImportError( + "The '{target}' package is required; " + "normally this is bundled with this add-on so if you get " + "this warning, consult the packager of your " + "distribution.".format(**locals()) + ) + + def install(self): + """ + Install this importer into sys.meta_path if not already present. + """ + if self not in sys.meta_path: + sys.meta_path.append(self) + + +# Third-party add-on imports +###################################################################### + +def importAny(*modules): + """ + Import by name, providing multiple alternative names + + Common use case: Support all the different package names found + between 2.0 add-ons, 2.1 AnkiWeb releases, and 2.1 dev releases + + Raises: + ImportError -- Module not found + + Returns: + module -- Imported python module + """ + for mod in modules: + try: + return __import__(mod) + except ImportError: + pass + raise ImportError("Requires one of " + ', '.join(modules)) + + + +# Registering external libraries & modules +###################################################################### + +# NOTE: Use of these is discouraged and should be reserved for cases where +# traditional vendoring fails or is not feasible to implement. + +# NOTE: I have yet to find a reliable way to add modules to packages +# that *do* ship with Anki, but are missing specific sub-modules +# (e.g. 'version' module of 'distutils') +# +# The issue mainly arises when Anki has already loaded the corresponding +# module at add-on init time. At that point it becomes non-trivial to +# update the module cache with our own version of the module. +# +# It is easy to make due with explicitly importing our own version of +# the module if we have control over the code-base, but in case of +# dependencies of other modules this becomes a major problem +# (e.g. third-party packages depending on stdlib modules missing +# in Anki's Python distribution). + +STRINGTYPES = (str, unicode) if ANKI20 else (str,) # noqa: F821 +LOOKUP_SUBDIRS = ["common", "anki20" if ANKI20 else "anki21"] + +def _addPathToModuleLookup(path): + # Insert at idx 0 in order to supersede system-wide packages + sys.path.insert(0, path) + +def addPathToModuleLookup(path): + """ + Add modules shipped with the add-on to Python module search path + + Arguments: + path {str,unicode} -- Fully qualified path to module directory + """ + assert isinstance(path, STRINGTYPES) + assert os.path.isdir(path) + _addPathToModuleLookup(path) + +def addSubdirPathToModuleLookup(path): + """ + Recursively add module directory with version-specific subfolders + to Python module search path + + Arguments: + path {str,unicode} -- Fully qualified path to module directory with + one or more of the following subfolders: + python2, python3, common + """ + assert isinstance(path, STRINGTYPES) + assert os.path.isdir(path) + # TODO: refactor + for path in [os.path.join(path, subdir) for subdir in LOOKUP_SUBDIRS]: + if not os.path.isdir(path): + continue + _addPathToModuleLookup(path) + + +# Installing binary dependencies (that are either part of a +# packaged module or standalone executables) +###################################################################### + +class BinaryInstaller(object): + + def __init__(self): + raise NotImplementedError diff --git a/src/image_occlusion_enhanced/libaddon/platform.py b/src/image_occlusion_enhanced/libaddon/platform.py new file mode 100644 index 00000000..3029146a --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/platform.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Provides information on Anki version and platform +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +import sys +import os + +from aqt import mw + +from anki import version as anki_version +from anki.utils import isMac, isWin + +from .utils import ensureExists + +if isMac: + PLATFORM = "mac" +elif isWin: + PLATFORM = "win" +else: + PLATFORM = "lin" + +SYS_ENCODING = sys.getfilesystemencoding() +PYTHON3 = sys.version_info[0] == 3 +ANKI20 = anki_version.startswith("2.0.") + +name_components = __name__.split(".") + +MODULE_ADDON = name_components[0] +MODULE_LIBADDON = name_components[1] + +PATH_ADDONS = mw.pm.addonFolder() + +if ANKI20: + JSPY_BRIDGE = "py.link" +else: + JSPY_BRIDGE = "pycmd" + +PATH_THIS_ADDON = os.path.join(PATH_ADDONS, MODULE_ADDON) + + +def schedVer(): + if ANKI20: + return 1 + if not mw.col: # collection not loaded + return None + return mw.col.schedVer() + + +def pathUserFiles(): + user_files = os.path.join(PATH_THIS_ADDON, "user_files") + return ensureExists(user_files) + + +def pathMediaFiles(): + return mw.col.media.dir() + + +def checkAnkiVersion(lower, upper=None): + """Check whether anki version is in specified range + + By default the upper boundary is set to infinite + + Arguments: + lower {str} -- minimum version (inclusive) + + Keyword Arguments: + upper {str} -- maximum version (exclusive) (default: {None}) + + Returns: + bool -- Whether anki version is in specified range + """ + return checkVersion(anki_version, lower, upper=upper) + + +def checkQtVersion(lower, upper=None): + """Check whether Qt version is in specified range + + By default the upper boundary is set to infinite + + Arguments: + lower {str} -- minimum version (inclusive) + + Keyword Arguments: + upper {str} -- maximum version (exclusive) (default: {None}) + + Returns: + bool -- Whether Qt version is in specified range + """ + from aqt.qt import QT_VERSION_STR + return checkVersion(QT_VERSION_STR, lower, upper=upper) + + +def checkVersion(current, lower, upper=None): + """Generic version checker + + Checks whether specified version is in specified range + + Arguments: + current {str} -- current version + lower {str} -- minimum version (inclusive) + + Keyword Arguments: + upper {str} -- maximum version (exclusive) (default: {None}) + + Returns: + bool -- Whether current version is in specified range + """ + from ._vendor.packaging import version + + if upper is not None: + ankiv_parsed = version.parse(current) + return (ankiv_parsed >= version.parse(lower) and + ankiv_parsed < version.parse(upper)) + + return version.parse(current) >= version.parse(lower) diff --git a/src/image_occlusion_enhanced/libaddon/util/__init__.py b/src/image_occlusion_enhanced/libaddon/util/__init__.py new file mode 100644 index 00000000..5e6da1bb --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/util/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Utility modules not specific to Anki +""" diff --git a/src/image_occlusion_enhanced/libaddon/util/filesystem.py b/src/image_occlusion_enhanced/libaddon/util/filesystem.py new file mode 100644 index 00000000..f4494cff --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/util/filesystem.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +File system manipulation utilities +""" + +import os +import sys + +from .types import PathOrString + + +def ensureExists(path: PathOrString) -> str: + path = str(path) + if not os.path.exists(path): + os.makedirs(path) + return path + + +def openFile(path: PathOrString) -> None: + """Open file in default viewer""" + import subprocess + + path = str(path) + + if sys.platform.startswith("win32"): + try: + os.startfile(path) # type: ignore + except (OSError, UnicodeDecodeError): + pass + elif sys.platform.startswith("darwin"): + subprocess.call(("open", path)) + else: + subprocess.call(("xdg-open", path)) diff --git a/src/image_occlusion_enhanced/libaddon/util/nesting.py b/src/image_occlusion_enhanced/libaddon/util/nesting.py new file mode 100644 index 00000000..8b9166da --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/util/nesting.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Manipulation of nested data structures +""" + +from functools import reduce +from copy import deepcopy + +from typing import Union, Any + +# Utility functions for operating with nested objects + + +def getNestedValue(obj: Any, keys: Union[list, tuple]): + """ + Get value out of nested collection by supplying tuple of + nested keys/indices + + Arguments: + obj {Collection} -- Nested collection + keys {list/tuple} -- Key/index path leading to config val + + Returns: + Any -- Config value + """ + cur = obj + for nr, key in enumerate(keys): + cur = cur[key] + return cur + + +def setNestedValue(obj: Any, keys: Union[list, tuple], value) -> None: + """ + Set value in nested collection by supplying Sequence of + nested keys / indices, and value to set + + Arguments: + obj {Collection} -- Nested collection + keys {list/tuple} -- Key/index path leading to config val + value {Any} -- value + """ + depth = len(keys) - 1 + cur = obj + for nr, key in enumerate(keys): + if nr == depth: + cur[key] = value + return + cur = cur[key] + + +def getNestedAttribute(obj: Any, attr: str, *args) -> Any: + """ + Gets nested attribute from "."-separated string + + Arguments: + obj {object} -- object to parse + attr {string} -- attribute name, optionally including + "."-characters to denote different levels + of nesting + + Returns: + Any -- object corresponding to attribute name + + Credits: + https://gist.github.com/wonderbeyond/d293e7a2af1de4873f2d757edd580288 + """ + + def _getattr(obj: Any, attr: str): + return getattr(obj, attr, *args) + + return reduce(_getattr, [obj] + attr.split(".")) + + +def deepMergeLists(original: list, incoming: list, new: bool = False) -> list: + """ + Deep merge two lists. Optionally leaves original intact. + + Procedure: + Reursively call deep merge on each correlated element of list. + If item type in both elements are + a. dict: Call deepMergeDicts on both values. + b. list: Call deepMergeLists on both values. + c. any other type: Value is overridden. + d. conflicting types: Value is overridden. + + If incoming list longer than original then extra values are appended. + + Arguments: + original {list} -- original list + incoming {list} -- list with updated values + new {bool} -- whether or not to create a new list instead of + updating original + + Returns: + list -- Merged list + + Credits: + https://stackoverflow.com/a/50773244/1708932 + """ + result = original if not new else deepcopy(original) + + common_length = min(len(original), len(incoming)) + for idx in range(common_length): + if isinstance(result[idx], dict) and isinstance(incoming[idx], dict): + deepMergeDicts(result[idx], incoming[idx]) + elif isinstance(result[idx], list) and isinstance(incoming[idx], list): + deepMergeLists(result[idx], incoming[idx]) + else: + result[idx] = incoming[idx] + + for idx in range(common_length, len(incoming)): + result.append(incoming[idx]) + + return result + + +def deepMergeDicts(original: dict, incoming: dict, new: bool = False) -> dict: + """ + Deep merge two dictionaries. Optionally leaves original intact. + + Procedure: + For key conflicts if both values are: + a. dict: Recursively call deepMergeDicts on both values. + b. list: Call deepMergeLists on both values. + c. any other type: Original value is overridden. + d. conflicting types: Original value is preserved. + + In the context of Anki config objects: + - original should correspond to default config, i.e. the "scheme" + of the expected config values + - incoming should correspond to the user-specific values + - incoming values takes precedence over original values with the + exception of: + - new values added to the configuration + - existing values whose data types have changed (e.g. list → dict) + + Arguments: + original {dict} -- original dictionary + incoming {dict} -- dictionary with updated values + new {bool} -- whether or not to create a new dictionary instead of + updating original + + Returns: + dict -- Merged dictionaries + + Credits: + https://stackoverflow.com/a/50773244/1708932 + + """ + result = original if not new else deepcopy(original) + + for key in incoming: + if key in result: + if isinstance(result[key], dict) and isinstance(incoming[key], dict): + deepMergeDicts(result[key], incoming[key]) + elif isinstance(result[key], list) and isinstance(incoming[key], list): + deepMergeLists(result[key], incoming[key]) + elif result[key] is not None and (type(result[key]) != type(incoming[key])): + # switched to different data type, original takes precedence + # with the exception of None value in original being replaced + pass + else: + # type preserved. incoming takes precedence. + result[key] = incoming[key] + else: + result[key] = incoming[key] + + return result diff --git a/src/image_occlusion_enhanced/libaddon/util/packaging.py b/src/image_occlusion_enhanced/libaddon/util/packaging.py new file mode 100644 index 00000000..68d37b5e --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/util/packaging.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2020 Aristotelis P. +# Copyright (C) 2016 Jason R Coombs and other PyPA contributors +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Components related to packaging third-party code and libraries +with Anki add-ons +""" + +import os +import sys + +from types import ModuleType + +from typing import Optional + + +__all__ = ["importAny", "addPathToModuleLookup"] + +# Third-party add-on imports +###################################################################### + + +def importAny(*modules: str) -> Optional[ModuleType]: + """ + Import by name, providing multiple alternative names + + Common use case: Support all the different package names found + between 2.0 add-ons, 2.1 AnkiWeb releases, and 2.1 dev releases + + Raises: + ImportError -- Module not found + + Returns: + module -- Imported python module + """ + for mod in modules: + try: + return __import__(mod) + except ImportError: + pass + raise ImportError("Requires one of " + ", ".join(modules)) + + +# Registering external libraries & modules +###################################################################### + +# NOTE: Use of these is discouraged and should be reserved for cases where +# traditional vendoring fails or is not feasible to implement. + +# NOTE: I have yet to find a reliable way to add modules to packages +# that *do* ship with Anki, but are missing specific sub-modules +# (e.g. 'version' module of 'distutils') +# +# The issue mainly arises when Anki has already loaded the corresponding +# module at add-on init time. At that point it becomes non-trivial to +# update the module cache with our own version of the module. +# +# It is easy to make due with explicitly importing our own version of +# the module if we have control over the code-base, but in case of +# dependencies of other modules this becomes a major problem +# (e.g. third-party packages depending on stdlib modules missing +# in Anki's Python distribution). + + +def addPathToModuleLookup(path: str) -> None: + """ + Add modules shipped with the add-on to Python module search path + + Arguments: + path {str,unicode} -- Fully qualified path to module directory + """ + assert os.path.isdir(path) + # Insert at idx 0 in order to supersede system-wide packages + sys.path.insert(0, path) diff --git a/src/image_occlusion_enhanced/libaddon/util/types.py b/src/image_occlusion_enhanced/libaddon/util/types.py new file mode 100644 index 00000000..264933b5 --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/util/types.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +from pathlib import Path + +from typing import Union + + +ListOrTuple = Union[list, tuple] +PathOrString = Union[str, Path] diff --git a/src/image_occlusion_enhanced/libaddon/util/version.py b/src/image_occlusion_enhanced/libaddon/util/version.py new file mode 100644 index 00000000..1829687c --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/util/version.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Utilities for semantic version comparisons +""" + +from .._vendor.packaging import version + +from typing import Optional + + +def checkVersion(current: str, lower: str, upper: Optional[str] = None) -> bool: + """Generic version checker + + Checks whether specified version is in specified range + + Arguments: + current {str} -- current version + lower {str} -- minimum version (inclusive) + + Keyword Arguments: + upper {str} -- maximum version (exclusive) (default: {None}) + + Returns: + bool -- Whether current version is in specified range + """ + + if upper is not None: + current_parsed = version.parse(current) + return current_parsed >= version.parse( + lower + ) and current_parsed < version.parse(upper) + + return version.parse(current) >= version.parse(lower) diff --git a/src/image_occlusion_enhanced/libaddon/utils.py b/src/image_occlusion_enhanced/libaddon/utils.py new file mode 100644 index 00000000..136d2b6e --- /dev/null +++ b/src/image_occlusion_enhanced/libaddon/utils.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- + +# Libaddon for Anki +# +# Copyright (C) 2018-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Miscellaneuos utilities used around libaddon +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +import os + +from functools import reduce +from copy import deepcopy + +# Utility functions for operating with nested objects + + +def getNestedValue(obj, keys): + """ + Get value out of nested collection by supplying tuple of + nested keys/indices + + Arguments: + obj {list/dict} -- Nested collection + keys {tuple} -- Key/index path leading to config val + + Returns: + object -- Config value + """ + cur = obj + for nr, key in enumerate(keys): + cur = cur[key] + return cur + + +def setNestedValue(obj, keys, value): + """ + Set value in nested collection by supplying tuple of + nested keys / indices, and value to set + + Arguments: + obj {list/dict} -- Nested collection + keys {tuple} -- Tuple of keys/indices + value {object} -- Key/index path leading to config val + """ + depth = len(keys) - 1 + cur = obj + for nr, key in enumerate(keys): + if nr == depth: + cur[key] = value + return + cur = cur[key] + + +def getNestedAttribute(obj, attr, *args): + """ + Gets nested attribute from "."-separated string + + Arguments: + obj {object} -- object to parse + attr {string} -- attribute name, optionally including + "."-characters to denote different levels + of nesting + + Returns: + object -- object corresponding to attribute name + + Credits: + https://gist.github.com/wonderbeyond/d293e7a2af1de4873f2d757edd580288 + """ + def _getattr(obj, attr): + return getattr(obj, attr, *args) + return reduce(_getattr, [obj] + attr.split('.')) + + +def deepMergeLists(original, incoming, new=False): + """ + Deep merge two lists. Optionally leaves original intact. + + Procedure: + Reursively call deep merge on each correlated element of list. + If item type in both elements are + a. dict: Call deepMergeDicts on both values. + b. list: Call deepMergeLists on both values. + c. any other type: Value is overridden. + d. conflicting types: Value is overridden. + + If incoming list longer than original then extra values are appended. + + Arguments: + original {list} -- original list + incoming {list} -- list with updated values + new {bool} -- whether or not to create a new list instead of + updating original + + Returns: + list -- Merged list + + Credits: + https://stackoverflow.com/a/50773244/1708932 + """ + result = original if not new else deepcopy(original) + + common_length = min(len(original), len(incoming)) + for idx in range(common_length): + if (isinstance(result[idx], dict) and + isinstance(incoming[idx], dict)): + deepMergeDicts(result[idx], incoming[idx]) + elif (isinstance(result[idx], list) and + isinstance(incoming[idx], list)): + deepMergeLists(result[idx], incoming[idx]) + else: + result[idx] = incoming[idx] + + for idx in range(common_length, len(incoming)): + result.append(incoming[idx]) + + return result + + +def deepMergeDicts(original, incoming, new=False): + """ + Deep merge two dictionaries. Optionally leaves original intact. + + Procedure: + For key conflicts if both values are: + a. dict: Recursively call deepMergeDicts on both values. + b. list: Call deepMergeLists on both values. + c. any other type: Original value is overridden. + d. conflicting types: Original value is preserved. + + In the context of Anki config objects: + - original should correspond to default config, i.e. the "scheme" + of the expected config values + - incoming should correspond to the user-specific values + - incoming values takes precedence over original values with the + exception of: + - new values added to the configuration + - existing values whose data types have changed (e.g. list → dict) + + Arguments: + original {list} -- original dictionary + incoming {list} -- dictionary with updated values + new {bool} -- whether or not to create a new dictionary instead of + updating original + + Returns: + dict -- Merged dictionaries + + Credits: + https://stackoverflow.com/a/50773244/1708932 + + """ + result = original if not new else deepcopy(original) + + for key in incoming: + if key in result: + if (isinstance(result[key], dict) and + isinstance(incoming[key], dict)): + deepMergeDicts(result[key], incoming[key]) + elif (isinstance(result[key], list) and + isinstance(incoming[key], list)): + deepMergeLists(result[key], incoming[key]) + elif (result[key] is not None and + (type(result[key]) != type(incoming[key]))): + # switched to different data type, original takes precedence + # with the exception of None value in original being replaced + pass + else: + # type preserved. incoming takes precedence. + result[key] = incoming[key] + else: + result[key] = incoming[key] + + return result + + +# File system manipulation + +def ensureExists(path): + if not os.path.exists(path): + os.makedirs(path) + return path + + +def openFile(path): + """Open file in default viewer""" + import subprocess + from .platform import PLATFORM + if PLATFORM == "win": + try: + os.startfile(path) + except (OSError, UnicodeDecodeError): + pass + elif PLATFORM == "mac": + subprocess.call(('open', path)) + else: + subprocess.call(("xdg-open", path)) diff --git a/src/image_occlusion_enhanced/locale/anki-image-occlusion-enhanced.pot b/src/image_occlusion_enhanced/locale/anki-image-occlusion-enhanced.pot new file mode 100644 index 00000000..26b61409 --- /dev/null +++ b/src/image_occlusion_enhanced/locale/anki-image-occlusion-enhanced.pot @@ -0,0 +1,667 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-12-14 02:53+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: ../add.py:80 +msgid "Non-editable note.
      Using image to create new IO note." +msgstr "" + +#: ../add.py:84 +msgid "" +"This note cannot be edited, nor is there
      an image to use for an image " +"occlusion." +msgstr "" + +#: ../add.py:95 ../add.py:267 +#, python-brace-format +msgid "Unsupported image in file {image_path}:

      {error}" +msgstr "" + +#: ../add.py:118 +msgid "Editing unavailable: Invalid image occlusion Note ID" +msgstr "" + +#: ../add.py:121 +msgid "Editing unavailable: Missing image or original mask" +msgstr "" + +#: ../add.py:164 +msgid "Select an Image" +msgstr "" + +#: ../add.py:165 +#, python-brace-format +msgid "Image Files ({file_glob_list})" +msgstr "" + +#: ../add.py:175 +msgid "Invalid image file path" +msgstr "" + +#: ../add.py:199 +msgid "Launching new ImgOccEdit instance" +msgstr "" + +#: ../config.py:93 +msgid "Updating config DB from earlier IO release" +msgstr "" + +#: ../dialogs.py:39 +#, python-brace-format +msgid "" +"\n" +"

      Basic Instructions

      \n" +"
        \n" +"
      1. With the rectangle tool or any other shape tool selected, cover the " +"areas of the image you want to be tested on
      2. \n" +"
      3. (Optional): Fill out additional information about your cards by " +"switching to the Fields tab
      4. \n" +"
      5. Click on one of the Add Cards buttons at the bottom of the " +"window to add the cards to your collection
      6. \n" +"
      \n" +"

      Drawing Custom Labels

      \n" +"
        \n" +"
      1. Draw up the layers sidepanel by clicking on the Layers button " +"at the right edge of the editor
      2. \n" +"
      3. Switch to the Labels layer by left-clicking on it. You can also " +"switch to the labels layer directly by using Ctrl + Shift + L.
      4. \n" +"
      5. Anything you draw in this layer – be it text, lines, or shapes – will " +"appear above the image, but still below your masks. All of the painting " +"tools in the left sidebar are at your disposal.
      6. \n" +"
      7. Switch back to the masks layer, either via the Layers " +"sidepanel, or by using the Ctrl + Shift + M " +"hotkey.
      8. \n" +"
      \n" +"

      Grouping Shapes

      \n" +"
        \n" +"
      1. Select multiple shapes, either by drawing a selection rectangle with the " +"selection tool active (S), or by shift-clicking on multiple " +"shapes
      2. \n" +"
      3. Either use the G hotkey or the Group Elements tool " +"in the top-bar to group your items
      4. \n" +"
      \n" +"

      Grouped shapes will form a single card.

      \n" +"

      More Information

      \n" +"

      For more information please refer to the following resources:

      \n" +"\n" +msgstr "" + +#: ../dialogs.py:68 +#, python-brace-format +msgid "" +"\n" +"Instructions for editing:\n" +"

      Each mask shape represents a card.\n" +"Removing any of the existing shapes will remove the corresponding card.\n" +"New shapes will generate new cards. You can change the occlusion type\n" +"by using the dropdown box on the left.

      If you click on the\n" +"Add new cards button a completely new batch of cards will be\n" +"generated, leaving your originals untouched.

      \n" +"Actions performed in Image Occlusion's Editing Mode cannot be\n" +"easily undone, so please make sure to check your changes twice before\n" +"applying them.

      The only exception to this are purely textual\n" +"changes to fields like the header or footer of your notes. These can\n" +"be fully reverted by using Ctrl+Z in the Browser or Reviewer view.

      \n" +"More information: Wiki: Editing Notes.\n" +msgstr "" + +#: ../dialogs.py:84 +#, python-brace-format +msgid "" +"\n" +"Fixing a broken note type:\n" +"

      The Image Occlusion Enhanced note type can't be edited\n" +"arbitrarily. If you delete a field that's required by the add-on\n" +"or rename it outside of the IO Options dialog you will be presented\n" +"with an error message.

      To fix this issue please follow the\n" +"instructions in the\n" +"wiki." +msgstr "" + +#: ../dialogs.py:93 +#, python-brace-format +msgid "" +"\n" +"

      Help and Support

      \n" +"

      Image Occlusion Enhanced Wiki

      \n" +"

      Official Video Tutorial Series

      \n" +"

      Support Thread

      \n" +"

      Credits and License

      \n" +"

      Copyright © 2012-2015\n" +"Tiago Barroso

      \n" +"

      Copyright © 2013\n" +"Steve AW

      \n" +"

      Copyright © 2016-2017\n" +"Aristotelis P.

      \n" +"

      Image Occlusion Enhanced is licensed under the GNU AGPLv3.

      \n" +"

      Third-party open-source software shipped with Image Occlusion " +"Enhanced:

      \n" +"\n" +msgstr "" + +#: ../dialogs.py:118 +#, python-brace-format +msgid "" +"\n" +"Important

      \n" +"The \"Hide All, Reveal All\" image occlusion mode used by this card\n" +"is no longer supported by the add-on. You can still review it just like\n" +"you would with any other card, but if you proceed with editing the note,\n" +"it will automatically be converted to the \"Hide All, Guess One\" type." +"

      \n" +"For more information on why this occlusion mode was removed and how to\n" +"replicate its functionality please see here:

      \n" +"
      Wiki: Hide All, Reveal All\n" +msgstr "" + +#: ../dialogs.py:130 +msgid "" +"Error: Image Occlusion note type not configured properly. Please make " +"sure you did not manually delete or rename any of the default fields." +msgstr "" + +#: ../dialogs.py:135 +msgid "" +"This is a purely experimental feature that is meant to update older\n" +"IO notes to be compatible with the new editing feature-set in IO Enhanced.\n" +"Clicking on 'Yes' below will prompt the add-on to go through all selected\n" +"notes and change their Note ID and mask files in a way that should make it\n" +"possible to edit them in the future.\n" +"

      Please note that this will only work for notes\n" +"that have already been switched to the Image Occlusion Enhanced note " +"type.\n" +"If you are coming from IO 2.0 or an older version of IO Enhanced you will\n" +"first have to switch the note type of your notes manually by going to " +"Edit →\n" +"Change Note Type.

      \n" +"WARNING: There is no guarantee that this feature will actually " +"succeed in\n" +"updating your notes properly. To convert legacy notes the add-on will have " +"to\n" +"make a few assumptions which in some rare instances might turn out to be " +"wrong\n" +"and lead to broken notes. Notes that can't be parsed for the information " +"needed\n" +"to convert into an editable state (e.g. a valid \"Original Mask\" field) " +"will usually\n" +"be skipped by the add-on, but there might be some corner cases where that " +"won't work.\n" +"

      A checkpoint will be set to revert to if needed,\n" +"but even with that safety measure in place you should still only use this\n" +"function if you know what you are doing.\n" +"

      Continue anyway?
      (Depending on the number of notes this " +"might\n" +"take a while)\n" +msgstr "" + +#: ../dialogs.py:162 +msgid "Image Occlusion Enhanced Error" +msgstr "" + +#: ../dialogs.py:180 ../dialogs.py:203 +msgid "Image Occlusion Enhanced" +msgstr "" + +#: ../dialogs.py:218 +msgid "Image Occlusion Enhanced Help" +msgstr "" + +#: ../editor.py:74 +#, python-brace-format +msgid "unknown action: {action_name}" +msgstr "" + +#: ../editor.py:121 +msgid "Tags" +msgstr "" + +#: ../editor.py:146 +msgid "Change &Image" +msgstr "" + +#: ../editor.py:150 +msgid "&Help" +msgstr "" + +#: ../editor.py:154 +msgid "Don't Change" +msgstr "" + +#: ../editor.py:155 +msgid "Hide All, Guess One" +msgstr "" + +#: ../editor.py:157 +msgid "Hide One, Guess One" +msgstr "" + +#: ../editor.py:160 +msgid "&Edit Cards" +msgstr "" + +#: ../editor.py:162 +msgid "&Add New Cards" +msgstr "" + +#: ../editor.py:164 +msgid "Hide &All, Guess One" +msgstr "" + +#: ../editor.py:166 +msgid "Hide &One, Guess One" +msgstr "" + +#: ../editor.py:168 +msgid "&Close" +msgstr "" + +#: ../editor.py:171 +msgid "" +"Switch to a different image while preserving all of the shapes and fields" +msgstr "" + +#: ../editor.py:173 +msgid "Preserve existing occlusion type" +msgstr "" + +#: ../editor.py:174 +msgid "Edit all cards using current mask shapes and field entries" +msgstr "" + +#: ../editor.py:176 +msgid "Create new batch of cards without editing existing ones" +msgstr "" + +#: ../editor.py:177 +msgid "" +"Generate cards with nonoverlapping information, where all
      labels are " +"hidden on the front and one revealed on the back" +msgstr "" + +#: ../editor.py:180 +msgid "" +"Generate cards with overlapping information, where one
      label is hidden on " +"the front and revealed on the back" +msgstr "" + +#: ../editor.py:182 +msgid "Close Image Occlusion Editor without generating cards" +msgstr "" + +#: ../editor.py:218 +msgid "Loading..." +msgstr "" + +#: ../editor.py:241 +msgid "&Masks Editor" +msgstr "" + +#: ../editor.py:242 +msgid "&Fields" +msgstr "" + +#: ../editor.py:244 +msgid "Include additional information (optional)" +msgstr "" + +#: ../editor.py:246 +msgid "Create image occlusion masks (required)" +msgstr "" + +#: ../editor.py:375 +msgid "Deck" +msgstr "" + +#: ../editor.py:376 +msgid "Image Occlusion Enhanced - Add Mode" +msgstr "" + +#: ../editor.py:377 +msgid "Add Cards:" +msgstr "" + +#: ../editor.py:387 +msgid "Deck for Add new cards" +msgstr "" + +#: ../editor.py:388 +msgid "Image Occlusion Enhanced - Editing Mode" +msgstr "" + +#: ../editor.py:389 +msgid "Type:" +msgstr "" + +#: ../main.py:47 +msgid "Please close Image Occlusion Editor to access the Options." +msgstr "" + +#: ../main.py:93 +msgid "Add Image Occlusion" +msgstr "" + +#: ../main.py:96 +msgid "Edit Image Occlusion" +msgstr "" + +#: ../main.py:101 +msgid "I/O" +msgstr "" + +#: ../main.py:155 +msgid "Occlude Image" +msgstr "" + +#: ../main.py:158 +msgid "Open Image" +msgstr "" + +#: ../main.py:248 +msgid "Image &Occlusion Enhanced Options..." +msgstr "" + +#: ../main.py:249 +msgid "Image &Occlusion Enhanced..." +msgstr "" + +#: ../nconvert.py:44 ../nconvert.py:60 +#, python-format +msgid "Skipping note that couldn't be parsed: %s" +msgstr "" + +#: ../nconvert.py:52 +#, python-format +msgid "Skipping note that we've just converted: %s" +msgstr "" + +#: ../nconvert.py:66 +#, python-brace-format +msgid "" +"{number_notes_updated} note(s) updated, {number_notes_skipped} " +"skipped" +msgstr "" + +#: ../nconvert.py:79 +#, python-format +msgid "Skipping note with wrong note type: %s" +msgstr "" + +#: ../nconvert.py:84 +#, python-format +msgid "Skipping IO note that is already editable: %s" +msgstr "" + +#: ../nconvert.py:89 +#, python-format +msgid "Skipping IO note without original SVG mask: %s" +msgstr "" + +#: ../nconvert.py:92 +#, python-format +msgid "Found IO note in need of update: %s" +msgstr "" + +#: ../nconvert.py:113 +msgid "Extracting data using IO 2.0 naming scheme" +msgstr "" + +#: ../nconvert.py:118 +msgid "Extracting data using IO Enhanced naming scheme" +msgstr "" + +#: ../nconvert.py:143 +#, python-format +msgid "Adding ID for note nr %s" +msgstr "" + +#: ../nconvert.py:158 +#, python-format +msgid "Setting om and tag for nid %s" +msgstr "" + +#: ../nconvert.py:212 +#, python-format +msgid "!saving %(node_id)s, %(mtype)s" +msgstr "" + +#: ../nconvert.py:226 +msgid "No cards selected." +msgstr "" + +#: ../nconvert.py:228 ../ngen.py:351 +msgid "Please confirm action" +msgstr "" + +#: ../nconvert.py:233 +msgid "Image Occlusion Note Conversions" +msgstr "" + +#: ../nconvert.py:248 +msgid "Convert to Editable IO &Enhanced Notes" +msgstr "" + +#: ../ngen.py:83 +msgid "No cards to generate.
      Are you sure you set your masks correctly?" +msgstr "" + +#: ../ngen.py:94 +msgid "Adding Image Occlusion Cards" +msgstr "" + +#: ../ngen.py:99 +#, python-brace-format +msgid "One card added" +msgid_plural "{card_count} cards added" +msgstr[0] "" +msgstr[1] "" + +#: ../ngen.py:114 +msgid "" +"No shapes left. You can't delete all cards.
      Are you sure you set your " +"masks correctly?" +msgstr "" + +#: ../ngen.py:117 +msgid "Editing Image Occlusion Cards" +msgstr "" + +#: ../ngen.py:158 +#, python-brace-format +msgid "One old card edited in place" +msgid_plural "{card_count} old cards edited in place" +msgstr[0] "" +msgstr[1] "" + +#: ../ngen.py:162 +#, python-brace-format +msgid "
      One existing card deleted" +msgid_plural "
      {card_count} existing cards deleted" +msgstr[0] "" +msgstr[1] "" + +#: ../ngen.py:166 +#, python-brace-format +msgid "
      One new card created" +msgid_plural "
      {card_count} new cards created" +msgstr[0] "" +msgstr[1] "" + +#: ../ngen.py:345 +#, python-brace-format +msgid "" +"This will delete {del_count} card(s) and create {new_count} new " +"one(s). Please note that this action is irreversible.

      Would you " +"still like to proceed?" +msgstr "" + +#: ../ngen.py:400 +#, python-format +msgid "!saving %(note_id)s, %(mtype)s" +msgstr "" + +#: ../ngen.py:449 +#, python-format +msgid "!noteflush %s" +msgstr "" + +#: ../ngen.py:452 +#, python-format +msgid "!notecreate %s" +msgstr "" + +#: ../options.py:55 +msgid "Please press the new key combination" +msgstr "" + +#: ../options.py:58 +msgid "Grab key combination" +msgstr "" + +#: ../options.py:77 +msgid "Please use at least one keyboard modifier (Ctrl, Alt, Shift)" +msgstr "" + +#: ../options.py:81 +msgid "Shift needs to be combined with at least one other modifier (Ctrl, Alt)" +msgstr "" + +#: ../options.py:85 +msgid "" +"Please press at least one key that is not a keyboard modifier (not Ctrl/Alt/" +"Shift)" +msgstr "" + +#: ../options.py:134 +msgid "Question mask" +msgstr "" + +#: ../options.py:135 +msgid "Other masks" +msgstr "" + +#: ../options.py:136 +msgid "Lines" +msgstr "" + +#: ../options.py:137 +msgid "Colors" +msgstr "" + +#: ../options.py:138 +msgid "Custom Field Names" +msgstr "" + +#: ../options.py:139 +msgid "Other Editor Settings" +msgstr "" + +#: ../options.py:151 +msgid "Line width" +msgstr "" + +#: ../options.py:152 +msgid "Label font" +msgstr "" + +#: ../options.py:153 +msgid "Label size" +msgstr "" + +#: ../options.py:169 +msgid "" +"Changing any of the entries below will rename the corresponding default " +"field of the IO Enhanced note type. This is the only way you can rename any " +"of the default fields.

      Renaming these fields through Anki's " +"regular dialogs will cause the add-on to fail. So please don't do that." +msgstr "" + +#: ../options.py:222 +msgid "Miscellaneous Settings" +msgstr "" + +#: ../options.py:225 +msgid "" +"Comma-separated list of fields to hide in Editing mode (in order to preserve " +"manual edits):" +msgstr "" + +#: ../options.py:231 +msgid "Invoke IO with the following hotkey:" +msgstr "" + +#: ../options.py:233 +msgid "Change hotkey" +msgstr "" + +#: ../options.py:247 +msgid "Restore &Defaults" +msgstr "" + +#: ../options.py:260 +msgid "Image Occlusion Enhanced Options" +msgstr "" + +#: ../options.py:345 +#, python-format +msgid "Renamed %(old_name)s, %(new_name)s" +msgstr "" + +#: ../options.py:358 +msgid "Field rename action aborted" +msgstr "" + +#: ../template.py:281 +msgid "Resetting IO Enhanced card template to defaults" +msgstr "" + +#: ../template.py:292 +msgid "Updating IO Enhanced card template" +msgstr "" + +#: ../utils.py:75 +msgid "Invalid SVG file." +msgstr "" + +#: ../utils.py:88 +msgid "Unrecognized raster image format." +msgstr "" + +#: ../utils.py:92 +msgid "Image has invalid dimensions." +msgstr "" diff --git a/src/image_occlusion_enhanced/locale/de/LC_MESSAGES/anki-image-occlusion-enhanced.po b/src/image_occlusion_enhanced/locale/de/LC_MESSAGES/anki-image-occlusion-enhanced.po new file mode 100644 index 00000000..51b6e962 --- /dev/null +++ b/src/image_occlusion_enhanced/locale/de/LC_MESSAGES/anki-image-occlusion-enhanced.po @@ -0,0 +1,824 @@ +# German Translation for Image Occlusion Enhanced. +# Copyright (C) 2020 glutanimate +# This file is distributed under the same license as the image-occlusion-enhanced package. +# Noxgrim <2074494+Noxgrim@users.noreply.github.com>, 2020. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: IOE 0.3\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-12-14 02:53+0100\n" +"PO-Revision-Date: 2020-01-18 18:41+1\n" +"Last-Translator: Noxgrim <2074494+Noxgrim@users.noreply.github.com>\n" +"Language-Team: None\n" +"Language: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural= n != 1;\n" + +#: ../add.py:80 +msgid "Non-editable note.
      Using image to create new IO note." +msgstr "Nicht bearbeitbare Notiz.
      Benutze Bild um BV-Notiz zu erstellen." + +#: ../add.py:84 +msgid "" +"This note cannot be edited, nor is there
      an image to use for an image " +"occlusion." +msgstr "" +"Diese Notiz kann weder bearbeitet werden, noch gibt es
      " +"ein Bild das für eine Bildverdeckung verwendet werden kann." + +#: ../add.py:95 ../add.py:267 +#, python-brace-format +msgid "Unsupported image in file {image_path}:

      {error}" +msgstr "Nicht unterstütztes Dateiformat {image_path}:

      {error}" + +#: ../add.py:118 +msgid "Editing unavailable: Invalid image occlusion Note ID" +msgstr "Bearbeiten nicht möglich: Ungültige Bildverdeckungsnotiz-ID" + +#: ../add.py:121 +msgid "Editing unavailable: Missing image or original mask" +msgstr "Bearbeiten nicht möglich: Fehlendes Bild oder ursprüngliche Maske" + +#: ../add.py:164 +msgid "Select an Image" +msgstr "Bildauswahl" + +#: ../add.py:165 +#, python-brace-format +msgid "Image Files ({file_glob_list})" +msgstr "Bilddateien ({file_glob_list})" + +#: ../add.py:175 +msgid "Invalid image file path" +msgstr "Ungültiger Dateipfad" + +#: ../add.py:199 +msgid "Launching new ImgOccEdit instance" +msgstr "Starte neue „ImgOccEdit“-Instanz" + +#: ../config.py:93 +msgid "Updating config DB from earlier IO release" +msgstr "Aktualisiere Einstellungsdatenbank von älterer Version" + +#: ../dialogs.py:39 +#, python-brace-format +msgid "" +"\n" +"

      Basic Instructions

      \n" +"
        \n" +"
      1. With the rectangle tool or any other shape tool selected, cover the " +"areas of the image you want to be tested on
      2. \n" +"
      3. (Optional): Fill out additional information about your cards by " +"switching to the Fields tab
      4. \n" +"
      5. Click on one of the Add Cards buttons at the bottom of the " +"window to add the cards to your collection
      6. \n" +"
      \n" +"

      Drawing Custom Labels

      \n" +"
        \n" +"
      1. Draw up the layers sidepanel by clicking on the Layers button " +"at the right edge of the editor
      2. \n" +"
      3. Switch to the Labels layer by left-clicking on it. You can also " +"switch to the labels layer directly by using Ctrl + Shift + L.
      4. \n" +"
      5. Anything you draw in this layer – be it text, lines, or shapes – will " +"appear above the image, but still below your masks. All of the painting " +"tools in the left sidebar are at your disposal.
      6. \n" +"
      7. Switch back to the masks layer, either via the Layers " +"sidepanel, or by using the Ctrl + Shift + M " +"hotkey.
      8. \n" +"
      \n" +"

      Grouping Shapes

      \n" +"
        \n" +"
      1. Select multiple shapes, either by drawing a selection rectangle with the " +"selection tool active (S), or by shift-clicking on multiple " +"shapes
      2. \n" +"
      3. Either use the G hotkey or the Group Elements tool " +"in the top-bar to group your items
      4. \n" +"
      \n" +"

      Grouped shapes will form a single card.

      \n" +"

      More Information

      \n" +"

      For more information please refer to the following resources:

      \n" +"\n" +msgstr "" +"\n" +"

      Einführung in die Grundlagen

      \n" +"
        \n" +"
      1. Wählen Sie das Rechtecks- oder ein anderes Formenwerkzeug aus und" +"verdecken Sie den Teil des Bildes, den Sie abfragen möchten
      2. \n" +"
      3. (Wahlweise): Geben Sie zusätzliche Informationen zu Ihren Karten an, indem " +"Sie zum Felder-Reiter wechseln
      4. \n" +"
      5. Klicken Sie auf eine der „Karten hinzufügen“-Schaltflächen im unteren Teil " +"des Fensters, um die Karten zu Ihrer Sammlung hinzuzufügen
      6. \n" +"
      \n" +"

      Benutzerdefinierte Beschriftungen hinzufügen

      \n" +"
        \n" +"
      1. Öffnen Sie den Ebeneneditor auf die Ebenen-Schaltfläche am rechten " +"Rand des Editors klicken
      2. \n" +"
      3. Wechseln Sie durch Klicken zur Labels-Ebene. Sie können " +"direkt zur Labels-Ebene wechseln, indem Sie Strg + Umschalt" +" + L drücken.
      4. \n" +"
      5. Alles was Sie auf dieser Ebene zeichnen – seien es Text, Linien oder Formen – wird " +"über dem Bild, aber immer noch unter Ihren Masken erscheinen. Ihnen stehen alle " +"Zeichenwerkzeuge in der linken Seite zur Verfügung.
      6. \n" +"
      7. Wechseln Sie zurück zur Maskenebene, entweder durch das Ebenen-Seitenpanel " +", oder durch die Tastenkombination Strg + Umschalt + M" +".
      8. \n" +"
      \n" +"

      Formen gruppieren

      \n" +"
        \n" +"
      1. Wählen Sie mehrere Formen, entweder indem Sie ein mit dem Auswahlwerkzeug ein Auswahlrechteck ziehen " +"(S) oder indem Sie mehrere Formen mit gedrückter" +"Umschalttaste anklicken.
      2. \n" +"
      3. Nutzen Sie entweder das G-Tastenkürzel oder das " +"„Gruppiere Elemente“-Werkzeug in der oberen Leiste, um Ihre Elemente " +"zu gruppieren.
      4. \n" +"
      \n" +"

      Gruppierte Formen werden zu einer einzelnen Karte.

      \n" +"

      Weitere Informationen

      \n" +"

      Für weitere Informationen können die folgenden (englischen) Ressourcen herangezogen werden:

      \n" +"\n" + +#: ../dialogs.py:68 +#, python-brace-format +msgid "" +"\n" +"Instructions for editing:\n" +"

      Each mask shape represents a card.\n" +"Removing any of the existing shapes will remove the corresponding card.\n" +"New shapes will generate new cards. You can change the occlusion type\n" +"by using the dropdown box on the left.

      If you click on the\n" +"Add new cards button a completely new batch of cards will be\n" +"generated, leaving your originals untouched.

      \n" +"Actions performed in Image Occlusion's Editing Mode cannot be\n" +"easily undone, so please make sure to check your changes twice before\n" +"applying them.

      The only exception to this are purely textual\n" +"changes to fields like the header or footer of your notes. These can\n" +"be fully reverted by using Ctrl+Z in the Browser or Reviewer view.

      \n" +"More information: Wiki: Editing Notes.\n" +msgstr "" +"\n" +"Anweisungen zum Bearbeiten:\n" +"

      Jede Maskenform stellt eine Karte dar.\n" +"Das Entfernen jeglicher existierender Formen entfernt die entsprechende Karte.\n" +"Neue Formen generieren neue Karten. Sie können den Verdeckungstyp ändern,\n" +"indem Sie das Aufklapp-Menü links benutzen.

      Wenn Sie auf die\n" +"„Neue Karten hinzufügen“-Schaltfläche klicken, wird ein komplett neuer Satz Karten\n" +"erstellt, wobei die ursprünglichen Karten unverändert bleiben.

      \n" +"Aktionen in „Image Occlusion“s Bearbeitungsmodus können nicht\n" +"einfach rückgängig gemacht werden, also überlegen Sie es sich bitte gut,\n" +"bevor Sie sie anwenden.

      Die einzige Ausnahme bilden reine Textänderungen\n" +"in den Feldern der Notizen, z.B. in der Kopf- oder Fußzeile. Diese können vollkommen\n" +"mit Ctrl+Z in der Kartenübersicht oder der Lernansicht rückgängig gemacht werden.

      \n" +"Weitere Informationen (englisch): Wiki: Notizen bearbeiten.\n" + +#: ../dialogs.py:84 +#, python-brace-format +msgid "" +"\n" +"Fixing a broken note type:\n" +"

      The Image Occlusion Enhanced note type can't be edited\n" +"arbitrarily. If you delete a field that's required by the add-on\n" +"or rename it outside of the IO Options dialog you will be presented\n" +"with an error message.

      To fix this issue please follow the\n" +"instructions in the\n" +"wiki." +msgstr "" +"\n" +"Reparieren eines beschädigten Notiztyps:\n" +"

      Der „Image Occlusion Enhanced“-Notiztyps kann nicht beliebig\n" +"bearbeitet werden. Wenn Sie ein von der Erweiterung benötigtes Feld entfernen oder\n" +"es außerhalb des IO-Einstellungsdialog umbenennen, erscheint eine Fehlermeldung.\n" +"

      Um dieses Problem zu lösen, folgen Sie bitten den Anweisungen\n" +" im (englischen) Wiki\n" +"." + +#: ../dialogs.py:93 +#, python-brace-format +msgid "" +"\n" +"

      Help and Support

      \n" +"

      Image Occlusion Enhanced Wiki

      \n" +"

      Official Video Tutorial Series

      \n" +"

      Support Thread

      \n" +"

      Credits and License

      \n" +"

      Copyright © 2012-2015\n" +"Tiago Barroso

      \n" +"

      Copyright © 2013\n" +"Steve AW

      \n" +"

      Copyright © 2016-2017\n" +"Aristotelis P.

      \n" +"

      Image Occlusion Enhanced is licensed under the GNU AGPLv3.

      \n" +"

      Third-party open-source software shipped with Image Occlusion " +"Enhanced:

      \n" +"\n" +msgstr "" +"\n" +"

      Hilfe und Unterstützung

      \n" +"

      (Alle Links führen zu englischen Ressourcen.)

      \n" +"

      „Image Occlusion Enhanced“-Wiki

      \n" +"

      Offizielle Videoanleitungen

      \n" +"

      Offizielles Hilfeforum

      \n" +"

      Danksagungen und Lizenzen

      \n" +"

      Copyright © 2012-2015\n" +"Tiago Barroso

      \n" +"

      Copyright © 2013\n" +"Steve AW

      \n" +"

      Copyright © 2016-2017\n" +"Aristotelis P.

      \n" +"

      Image Occlusion Enhanced ist unter der GNU AGPLv3 lizenziert.

      \n" +"

      Open-Source Software von Dritten, die mit Image Occlusion " +"Enhanced mitgeliefert wird:

      \n" +"\n" + +#: ../dialogs.py:118 +#, python-brace-format +msgid "" +"\n" +"Important

      \n" +"The \"Hide All, Reveal All\" image occlusion mode used by this card\n" +"is no longer supported by the add-on. You can still review it just like\n" +"you would with any other card, but if you proceed with editing the note,\n" +"it will automatically be converted to the \"Hide All, Guess One\" type." +"

      \n" +"For more information on why this occlusion mode was removed and how to\n" +"replicate its functionality please see here:

      \n" +"
      Wiki: Hide All, Reveal All\n" +msgstr "" +"\n" +"Wichtig

      \n" +"Der „Alle verdecken, alle aufdecken“-Bildverdeckungsmodus der von dieser Karte benutzt wird,\n" +"wird nicht mehr von der Erweiterung unterstützt. Sie können die Karten immer noch normal\n" +"wiederholen, aber wenn sie mit der Bearbeitung fortfahren, wird die Notiz,\n" +"automatisch in den „Alle verdecken, eine erraten“-Typ umgewandelt." +"

      \n" +"Mehr Informationen über den Grund der Entfernung dieses Bildverdeckungsmodus \n" +"und wie Sie dessen Verhalten nachbilden können finden Sie hier (englisch):

      \n" +"Wiki: Alle verdecken, alle aufdecken\n" + +#: ../dialogs.py:130 +msgid "" +"Error: Image Occlusion note type not configured properly. Please make " +"sure you did not manually delete or rename any of the default fields." +msgstr "" +"Fehler: Der „Image Occlusion“-Notiztyp ist nicht richtig konfiguriert. Bitte stellen " +"Sie sicher, dass Sie keine der Standardfelder gelöscht oder umbenannt haben." + +#: ../dialogs.py:135 +msgid "" +"This is a purely experimental feature that is meant to update older\n" +"IO notes to be compatible with the new editing feature-set in IO Enhanced.\n" +"Clicking on 'Yes' below will prompt the add-on to go through all selected\n" +"notes and change their Note ID and mask files in a way that should make it\n" +"possible to edit them in the future.\n" +"

      Please note that this will only work for notes\n" +"that have already been switched to the Image Occlusion Enhanced note " +"type.\n" +"If you are coming from IO 2.0 or an older version of IO Enhanced you will\n" +"first have to switch the note type of your notes manually by going to " +"Edit →\n" +"Change Note Type.

      \n" +"WARNING: There is no guarantee that this feature will actually " +"succeed in\n" +"updating your notes properly. To convert legacy notes the add-on will have " +"to\n" +"make a few assumptions which in some rare instances might turn out to be " +"wrong\n" +"and lead to broken notes. Notes that can't be parsed for the information " +"needed\n" +"to convert into an editable state (e.g. a valid \"Original Mask\" field) " +"will usually\n" +"be skipped by the add-on, but there might be some corner cases where that " +"won't work.\n" +"

      A checkpoint will be set to revert to if needed,\n" +"but even with that safety measure in place you should still only use this\n" +"function if you know what you are doing.\n" +"

      Continue anyway?
      (Depending on the number of notes this " +"might\n" +"take a while)\n" +msgstr "" +"Dies ist eine rein experimentelle Funktion, die dafür gedacht ist, ältere\n" +"BV-Notizen kompatibel mit dem neuen Funktionsumfang von „IO Enhanced“ zu machen.\n" +"Ein Klick auf „Ja“ (unten) weist die Erweiterung an, die Notiz-IDs und Maskendateien\n" +"aller ausgewählten Notizen so zu ändern, dass sie bearbeitbar werden \n" +"sollten.\n" +"

      Bitte bedenken Sie, dass dies nur für Notizen funktioniert, die\n" +"bereits auf den „Image Occlusion Enhanced“-Notiztyp geändert " +"wurden.\n" +"Wenn Sie vorher IOE 2.0 oder eine noch ältere Version benutzt haben,\n" +"müssen Sie den Typ Ihrer Notizen von Hand ändern, indem Sie " +"Notizen →\n" +"Notiztyp ändern… in der Kartenübersicht auswählen.

      \n" +"WARNUNG: Es kann nicht gewährleistet werden, dass die Funktion " +"erfolgreich Ihre\n" +"Notizen ändern wird. Um die veralteten Notizen zu aktualisieren, muss die " +"Erweiterung\n" +"ein paar Annahmen machen, die sich vielleicht als falsch herausstellen " +"und\n" +"zu beschädigten Notizen führen können. Notizen, von denen die zur Konvertierung\n" +"in einen bearbeitbaren Zustand benötigten Informationen (z.B. ein gültiges „Original Mask“-Feld)," +"\n" +" nicht ausgelesen werden können,\n" +"werden meist von der Erweiterung übersprungen, aber es könnte Randfälle\n" +"geben, bei denen diese Funktion nicht funktioniert.\n" +"

      Es wird zwar ein Rücksetzpunkt zum Wiederherstellen des Originalzustands angelegt,\n" +"aber Sie sollten diese Funktion trotzdem nur verwenden, wenn Sie wissen, was Sie tun.\n" +"

      Trotzdem fortfahren?
      (Je nachdem wie viele Notizen ausgewählt " +"sind wird dies vielleicht eine Weile dauern.)\n" + +#: ../dialogs.py:162 +msgid "Image Occlusion Enhanced Error" +msgstr "„Image Occlusion Enhanced“-Fehler" + +#: ../dialogs.py:180 ../dialogs.py:203 +msgid "Image Occlusion Enhanced" +msgstr "Image Occlusion Enhanced" + +#: ../dialogs.py:218 +msgid "Image Occlusion Enhanced Help" +msgstr "„Image Occlusion Enhanced“-Hilfe" + +#: ../editor.py:74 +#, python-brace-format +msgid "unknown action: {action_name}" +msgstr "Unbekannte Aktion: {action_name}" + +#: ../editor.py:120 +msgid "Tags" +msgstr "Schlagworte" + +#: ../editor.py:145 +msgid "Change &Image" +msgstr "&Bild ändern" + +#: ../editor.py:149 +msgid "&Help" +msgstr "&Hilfe" + +#: ../editor.py:153 +msgid "Don't Change" +msgstr "Unverändert lassen" + +#: ../editor.py:154 +msgid "Hide All, Guess One" +msgstr "Alle verdecken, eine erraten" + +#: ../editor.py:155 +msgid "Hide One, Guess One" +msgstr "Eine verdecken, eine erraten" + +#: ../editor.py:157 +msgid "&Edit Cards" +msgstr "Karten &bearbeiten" + +#: ../editor.py:159 +msgid "&Add New Cards" +msgstr "Neue Karten &hinzufügen" + +#: ../editor.py:161 +msgid "Hide &All, Guess One" +msgstr "&Alle verdecken, eine erraten" + +#: ../editor.py:163 +msgid "Hide &One, Guess One" +msgstr "&Eine verdecken, eine erraten" + +#: ../editor.py:165 +msgid "&Close" +msgstr "&Schließen" + +#: ../editor.py:168 +msgid "" +"Switch to a different image while preserving all of the shapes and fields" +msgstr "" +"Bild unter Beibehaltung aller Formen und Felder austauschen" + +#: ../editor.py:170 +msgid "Preserve existing occlusion type" +msgstr "Vorhandenen Verdeckungstyp beibehalten" + +#: ../editor.py:174 +msgid "Edit all cards using current mask shapes and field entries" +msgstr "Bearbeite alle Karten mit den aktuellen Maskenformen und Feldinhalten" + +#: ../editor.py:176 +msgid "Create new batch of cards without editing existing ones" +msgstr "Erstelle einen neuen Satz Karten ohne die vorhandenen zu bearbeiten" + +#: ../editor.py:177 +msgid "" +"Generate cards with nonoverlapping information, where all
      labels are " +"hidden on the front and one revealed on the back" +msgstr "" +"Erstelle Karten mit nicht-überlappenden Informationen, bei denen alle
      Beschriftungen " +"auf der Vorderseite verdeckt sind und eine davon auf der Rückseite aufgedeckt wird" + +#: ../editor.py:180 +msgid "" +"Generate cards with overlapping information, where one
      label is hidden on " +"the front and revealed on the back" +msgstr "" +"Erstelle Karten mit überlappenden Informationen, bei denen eine
      Beschriftung " +"auf der Vorderseite verdeckt ist, die dann auf der Rückseite aufgedeckt wird" + +#: ../editor.py:182 +msgid "Close Image Occlusion Editor without generating cards" +msgstr "Bildverdeckungs-Editor schließen, ohne Karten zu erstellen" + +#: ../editor.py:213 +msgid "Loading..." +msgstr "Editor wird geladen..." + +#: ../editor.py:236 +msgid "&Masks Editor" +msgstr "&Masken-Editor" + +#: ../editor.py:237 +msgid "&Fields" +msgstr "&Felder" + +#: ../editor.py:239 +msgid "Include additional information (optional)" +msgstr "Zusätzliche Informationen hinzufügen (auf Wunsch)" + +#: ../editor.py:241 +msgid "Create image occlusion masks (required)" +msgstr "Erstelle Bildverdeckungsmasken (erforderlich)" + +#: ../editor.py:370 +msgid "Deck" +msgstr "Stapel" + +#: ../editor.py:371 +msgid "Image Occlusion Enhanced - Add Mode" +msgstr "Image Occlusion Enhanced - „Hinzufügen“-Modus" + +#: ../editor.py:372 +msgid "Add Cards:" +msgstr "Karten hinzufügen:" + +#: ../editor.py:382 +msgid "Deck for Add new cards" +msgstr "Stapel für Neue Karten hinzufügen" + +#: ../editor.py:388 +msgid "Image Occlusion Enhanced - Editing Mode" +msgstr "Image Occlusion Enhanced - Bearbeitungsmodus" + +#: ../editor.py:389 +msgid "Type:" +msgstr "Typ:" + +#: ../main.py:47 +msgid "Please close Image Occlusion Editor to access the Options." +msgstr "Bitte schließen Sie den Image-Occlusion-Editor, um Zugriff auf die Einstellungen zu haben." + +#: ../main.py:93 +msgid "Add Image Occlusion" +msgstr "Bildverdeckung hinzufügen" + +#: ../main.py:96 +msgid "Edit Image Occlusion" +msgstr "Bildverdeckung bearbeiten" + +#: ../main.py:101 +msgid "I/O" +msgstr "B/V" + +#: ../main.py:155 +msgid "Occlude Image" +msgstr "Bild verdecken" + +#: ../main.py:158 +msgid "Open Image" +msgstr "Bild öffnen" + +#: ../main.py:248 +msgid "Image &Occlusion Enhanced Options..." +msgstr "„Image &Occlusion Enhanced“-Einstellungen ..." + +#: ../main.py:249 +msgid "Image &Occlusion Enhanced..." +msgstr "Image &Occlusion Enhanced..." + +#: ../nconvert.py:44 ../nconvert.py:59 +#, python-format +msgid "Skipping note that couldn't be parsed: %s" +msgstr "Überspringe Notiz, die nicht eingelesen werden konnte: %s" + +#: ../nconvert.py:51 +#, python-format +msgid "Skipping note that we've just converted: %s" +msgstr "Überspringe Notiz, die gerade konvertiert wurde: %s" + +#: ../nconvert.py:65 +#, python-brace-format +msgid "" +"{number_notes_updated} note(s) updated, {number_notes_skipped} " +"skipped" +msgstr "" +"{number_notes_updated} Notiz(en) aktualisiert, {number_notes_skipped} " +"übersprungen" + +#: ../nconvert.py:78 +#, python-format +msgid "Skipping note with wrong note type: %s" +msgstr "Überspringe Notiz mit falschem Typ: %s" + +#: ../nconvert.py:83 +#, python-format +msgid "Skipping IO note that is already editable: %s" +msgstr "Überspringe BV-Notiz, die bereits bearbeitbar ist: %s" + +#: ../nconvert.py:88 +#, python-format +msgid "Skipping IO note without original SVG mask: %s" +msgstr "Überspringe BV-Notiz ohne ursprüngliche SVG-Maske: %s" + +#: ../nconvert.py:91 +#, python-format +msgid "Found IO note in need of update: %s" +msgstr "BV-Notiz gefunden, die aktualisiert werden muss: %s" + +#: ../nconvert.py:112 +msgid "Extracting data using IO 2.0 naming scheme" +msgstr "Extrahiere Daten, die das „IOE 2.0“-Benennungsschema nutzen" + +#: ../nconvert.py:117 +msgid "Extracting data using IO Enhanced naming scheme" +msgstr "Extrahiere Daten, die das „IO Enhanced“-Benennungsschema nutzen" + +#: ../nconvert.py:143 +#, python-format +msgid "Adding ID for note nr %s" +msgstr "Füge ID für Notiz Nummer %s hinzu" + +#: ../nconvert.py:158 +#, python-format +msgid "Setting om and tag for nid %s" +msgstr "Setze VM und Schlagwort für NID %s" + +#: ../nconvert.py:212 +#, python-format +msgid "!saving %(node_id)s, %(mtype)s" +msgstr "!speichere %(node_id)s, %(mtype)s" + +#: ../nconvert.py:226 +msgid "No cards selected." +msgstr "Keine Karten ausgewählt." + +#: ../nconvert.py:228 ../ngen.py:351 +msgid "Please confirm action" +msgstr "Bitte bestätigen Sie diese Aktion" + +#: ../nconvert.py:233 +msgid "Image Occlusion Note Conversions" +msgstr "„Image Occlusion“-Notiz-Konvertierungen" + +#: ../nconvert.py:248 +msgid "Convert to Editable IO &Enhanced Notes" +msgstr "In bearbeitbare „IO Enhanced“-Notiz &konvertieren" + +#: ../ngen.py:83 +msgid "No cards to generate.
      Are you sure you set your masks correctly?" +msgstr "Keine Karten zu generieren.
      Sind Sie sicher, dass Sie Ihre Masken richtig gesetzt haben?" + +#: ../ngen.py:94 +msgid "Adding Image Occlusion Cards" +msgstr "Füge Bildverdeckungskarten hinzu" + +#: ../ngen.py:99 +#, python-brace-format +msgid "One card added" +msgid_plural "{card_count} cards added" +msgstr[0] "Eine Karte hinzugefügt" +msgstr[1] "{card_count} Karten hinzugefügt" + +#: ../ngen.py:114 +msgid "" +"No shapes left. You can't delete all cards.
      Are you sure you set your " +"masks correctly?" +msgstr "" +"Keine Formen übrig. Sie können nicht alle Karten löschen.
      " +"Sind Sie sicher, dass Sie Ihre Masken richtig gesetzt haben?" + +#: ../ngen.py:117 +msgid "Editing Image Occlusion Cards" +msgstr "Bearbeite Bildverdeckungskarten" + +#: ../ngen.py:158 +#, python-brace-format +msgid "One old card edited in place" +msgid_plural "{card_count} old cards edited in place" +msgstr[0] "Eine alte Karte direkt bearbeitet" +msgstr[1] "{card_count} alte Karten direkt bearbeitet" + +#: ../ngen.py:162 +#, python-brace-format +msgid "
      One existing card deleted" +msgid_plural "
      {card_count} existing cards deleted" +msgstr[0] "
      Eine vorhandene Karte gelöscht" +msgstr[1] "
      {card_count} vorhandene Karten gelöscht" + +#: ../ngen.py:166 +#, python-brace-format +msgid "
      One new card created" +msgid_plural "
      {card_count} new cards created" +msgstr[0] "
      Eine neue Karte erstellt" +msgstr[1] "
      {card_count} neue Karten erstellt" + +#: ../ngen.py:345 +#, python-brace-format +msgid "" +"This will delete {del_count} card(s) and create {new_count} new " +"one(s). Please note that this action is irreversible.

      Would you " +"still like to proceed?" +msgstr "" +"Es werden {del_count} Karte(n) entfernt und {new_count} neue Karte(n) " +"erstellt. Bitte bedenken Sie, dass dies nicht rückgängig gemacht werden kann.

      " +"Möchten Sie trotzdem fortfahren?" + +#: ../ngen.py:400 +#, python-format +msgid "!saving %(note_id)s, %(mtype)s" +msgstr "!speichere %(note_id)s, %(mtype)s" + +#: ../ngen.py:449 +#, python-format +msgid "!noteflush %s" +msgstr "!notizleeren %s" + +#: ../ngen.py:452 +#, python-format +msgid "!notecreate %s" +msgstr "!notizerstellen %s" + +#: ../options.py:55 +msgid "Please press the new key combination" +msgstr "Neue Tastenkombination eingeben" + +#: ../options.py:58 +msgid "Grab key combination" +msgstr "Tastenkombination erfassen" + +#: ../options.py:77 +msgid "Please use at least one keyboard modifier (Ctrl, Alt, Shift)" +msgstr "Bitte Nutzen Sie mindestens eine Sondertaste (Strg, Alt, Umschalt)" + +#: ../options.py:81 +msgid "Shift needs to be combined with at least one other modifier (Ctrl, Alt)" +msgstr "Die Umschalttaste muss mit mindestens einer weiteren Sondertaste kombiniert werden (Strg, Alt)" + +#: ../options.py:85 +msgid "" +"Please press at least one key that is not a keyboard modifier (not Ctrl/Alt/" +"Shift)" +msgstr "" +"Bitte drücken Sie mindestens eine Taste, die keine Sondertaste ist (also nicht Strg/Alt/" +"Umschalt)" + +#: ../options.py:134 +msgid "Question mask" +msgstr "Gefragte Maske" + +#: ../options.py:135 +msgid "Other masks" +msgstr "Andere Masken" + +#: ../options.py:136 +msgid "Lines" +msgstr "Linien" + +#: ../options.py:137 +msgid "Colors" +msgstr "Farben" + +#: ../options.py:138 +msgid "Custom Field Names" +msgstr "Benutzerdefinierte Feldnamen" + +#: ../options.py:139 +msgid "Other Editor Settings" +msgstr "Andere Editor-Einstellungen" + +#: ../options.py:151 +msgid "Line width" +msgstr "Linienbreite" + +#: ../options.py:152 +msgid "Label font" +msgstr "Schriftart der Beschriftungen" + +#: ../options.py:153 +msgid "Label size" +msgstr "Größe der Beschriftungen" + +#: ../options.py:169 +msgid "" +"Changing any of the entries below will rename the corresponding default " +"field of the IO Enhanced note type. This is the only way you can rename any " +"of the default fields.

      Renaming these fields through Anki's " +"regular dialogs will cause the add-on to fail. So please don't do that." +msgstr "" +"Das Ändern eines der unteren Einträge wird das entsprechende Standardfeld des " +"„IO Enhanced“-Notiztyps umbenennen. Dies ist der einzige Weg, diese Felder " +"umzubenennen.

      Das Umbenennen dieser Felder mittels Ankis " +"normaler Dialoge wird dazu führen, dass die Erweiterung nicht mehr funktioniert, " +"also sehen Sie bitte davon ab." + +#: ../options.py:221 +msgid "Miscellaneous Settings" +msgstr "Sonstige Einstellungen" + +#: ../options.py:225 +msgid "" +"Comma-separated list of fields to hide in Editing mode (in order to preserve " +"manual edits):" +msgstr "Durch Kommas abgetrennte Liste von Feldern, die im Bearbeitungsmodus versteckt werden " +"(um manuelle Änderungen zu erhalten)" + +#: ../options.py:231 +msgid "Invoke IO with the following hotkey:" +msgstr "BV mit folgender Tastenkombination öffnen:" + +#: ../options.py:233 +msgid "Change hotkey" +msgstr "Tastenkombination ändern" + +#: ../options.py:247 +msgid "Restore &Defaults" +msgstr "Standardeinstellungen wiederherstellen" + +#: ../options.py:260 +msgid "Image Occlusion Enhanced Options" +msgstr "„Image Occlusion Enhanced“-Einstellungen" + +#: ../options.py:345 +#, python-format +msgid "Renamed %(old_name)s, %(new_name)s" +msgstr "%(old_name)s in %(new_name)s unbenannt" + +#: ../options.py:358 +msgid "Field rename action aborted" +msgstr "Feldnamenumbenennung abgebrochen" + +#: ../template.py:281 +msgid "Resetting IO Enhanced card template to defaults" +msgstr "Setze „IO Enhanced“-Kartenvorlage auf Standardeinstellungen zurück" + +#: ../template.py:292 +msgid "Updating IO Enhanced card template" +msgstr "Aktualisiere „IO Enhanced“-Kartenvorlage" + +#: ../utils.py:75 +msgid "Invalid SVG file." +msgstr "Ungültige SVG-Datei." + +#: ../utils.py:88 +msgid "Unrecognized raster image format." +msgstr "Rasterbildformat wurde nicht erkannt." + +#: ../utils.py:92 +msgid "Image has invalid dimensions." +msgstr "Die Bildabmessungen (Größe, Breite) sind ungültig." diff --git a/src/image_occlusion_enhanced/main.py b/src/image_occlusion_enhanced/main.py new file mode 100644 index 00000000..5695125e --- /dev/null +++ b/src/image_occlusion_enhanced/main.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# Copyright (C) 2012-2015 Tiago Barroso +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Sets up buttons and menus and calls other modules. +""" + +import logging +import sys + +from anki.lang import _ as __ +from aqt.qt import * +from aqt.qt import QMenu + +from aqt import mw +from aqt.editor import Editor, EditorWebView +from aqt.addcards import AddCards +from aqt.editcurrent import EditCurrent +from aqt.reviewer import Reviewer +from aqt.utils import tooltip +from aqt.webview import AnkiWebView +from anki.hooks import wrap, addHook, runHook + +from .consts import * +from .config import * +from .add import ImgOccAdd +from .options import ImgOccOpts +from .dialogs import ioHelp, ioCritical +from .lang import _ + +logging.basicConfig(stream=sys.stdout, level=logging.ERROR) + + +def onIoSettings(): + """Call settings dialog if Editor not active""" + # TODO: fix ImgOccEdit detection + if hasattr(mw, "ImgOccEdit") and mw.ImgOccEdit.visible: + tooltip(_("Please close Image Occlusion Editor" " to access the Options.")) + return + dialog = ImgOccOpts() + dialog.exec_() + + +def onIoHelp(): + """Call main help dialog""" + ioHelp("main", parent=mw) + + +def onImgOccButton(self, origin=None, image_path=None): + """Launch Image Occlusion Enhanced""" + origin = origin or getEdParentInstance(self.parentWindow) + io_model = getOrCreateModel() + if io_model: + io_model_fields = mw.col.models.fieldNames(io_model) + if "imgocc" in mw.col.conf: + dflt_fields = list(mw.col.conf["imgocc"]["flds"].values()) + else: + dflt_fields = list(IO_FLDS.values()) + # note type integrity check + if not all(x in io_model_fields for x in dflt_fields): + ioCritical("model_error", help="notetype", parent=self.parentWindow) + return False + try: # allows us to fall back to old image if necessary + oldimg = self.imgoccadd.image_path + except AttributeError: + oldimg = None + self.imgoccadd = ImgOccAdd(self, origin, oldimg) + self.imgoccadd.occlude(image_path) + + +def onSetupEditorButtons(buttons, editor): + """Add IO button to Editor""" + conf = mw.pm.profile.get("imgocc") + if not conf: + hotkey = IO_HOTKEY + else: + hotkey = conf.get("hotkey", IO_HOTKEY) + + origin = getEdParentInstance(editor.parentWindow) + + if origin == "addcards": + tt = _("Add Image Occlusion") + icon_name = "add.png" + else: + tt = _("Edit Image Occlusion") + icon_name = "edit.png" + + icon = os.path.join(ICONS_PATH, icon_name) + + b = editor.addButton( + icon, + _("I/O"), + lambda o=editor: onImgOccButton(o), + tip="{} ({})".format(tt, hotkey), + keys=hotkey, + disables=False, + ) + + buttons.append(b) + return buttons + + +def getEdParentInstance(parent): + """Determine parent instance of editor widget""" + if isinstance(parent, AddCards): + return "addcards" + elif isinstance(parent, EditCurrent): + return "editcurrent" + else: + return "browser" + + +def openImage(path): + """Open path with default system app""" + import subprocess + + try: + if sys.platform == "win32": + subprocess.Popen(["explorer", path]) + elif sys.platform == "darwin": + subprocess.Popen(["open", path]) + else: + subprocess.Popen(["xdg-open", path]) + except OSError as e: + QDesktopServices.openUrl(QUrl("file://" + path)) + + +def maybe_add_image_menu(webview: AnkiWebView, menu: QMenu): + # cf. https://doc.qt.io/qt-5/qwebenginepage.html#contextMenuData + context_data = webview.page().contextMenuData() + url = context_data.mediaUrl() + image_name = url.fileName() + path = os.path.join(mw.col.media.dir(), image_name) + if url.isValid() and path: + a = menu.addAction(_("Occlude Image")) + a.triggered.connect( + lambda _, u=path, e=webview.editor: onImgOccButton(e, image_path=u) + ) + a = menu.addAction(_("Open Image")) + a.triggered.connect(lambda _, u=path: openImage(u)) + + +def legacyEditorContextMenuEvent(self, evt): + """Legacy: Monkey-patch context menu to add our own entries on Anki releases + that do not support the 'EditorWebView.contextMenuEvent' hook""" + m = QMenu(self) + a = m.addAction(__("Cut")) + a.triggered.connect(self.onCut) + a = m.addAction(__("Copy")) + a.triggered.connect(self.onCopy) + a = m.addAction(__("Paste")) + a.triggered.connect(self.onPaste) + ################################################## + maybe_add_image_menu(self, m) + ################################################## + runHook("EditorWebView.contextMenuEvent", self, m) + m.popup(QCursor.pos()) + + +io_editor_style = """ + +""" + + +def js_note_loaded(note) -> str: + js = [] + + # Conditionally set body CSS class + if not (note and note.model()["name"] == IO_MODEL_NAME): + js.append("""$("body").removeClass("ionote");""") + else: + # Only hide first field if it's the ID field + # TODO? identify ID field HTML element automatically + if note.model()["flds"][0]["name"] == IO_FLDS["id"]: + js.append("""$("body").addClass("ionote-id");""") + else: + js.append("""$("body").removeClass("ionote-id");""") + js.append("""$("body").addClass("ionote");""") + + return "\n".join(js) + + +def on_editor_will_load_note(js: str, note, editor): + """Customize the editor when IO notes are active""" + if not editor.web: + # editor is in cleanup TODO: evaluate if check still necessary + return js + js_additions = js_note_loaded(note) + return "\n".join([js, js_additions]) + + +def legacyOnSetNote(self, note, hide=True, focus=False): + """Legacy: Monkey-patch Editor.onSetNote + when 'editor_will_load_note' hook unavailable""" + if self.web is None: # editor is in cleanup + return + js = js_note_loaded(self.note) + self.web.eval(js) + + +def on_webview_will_set_content(web_content, context): + if not isinstance(context, Editor): + return + web_content.body += io_editor_style + + +def on_main_window_did_init(): + """Add our custom user styles to the editor HTML + Need to delay this to avoid interferences with other add-ons that might + potentially overwrite editor HTML""" + try: # 2.1.22+ + from aqt.gui_hooks import webview_will_set_content + + webview_will_set_content.append(on_webview_will_set_content) + except (ImportError, ModuleNotFoundError): + from aqt import editor + + editor._html = editor._html + io_editor_style.replace("%", "%%") + + +_profile_singleshot_run = False + + +def on_profile_loaded_singleshot(): + """Legacy single-shot function to delay execution of particular code paths + until Anki (and other add-ons) loaded""" + global _profile_singleshot_run + if _profile_singleshot_run: + return + on_main_window_did_init() + _profile_singleshot_run = True + + +def on_profile_loaded(): + """Setup add-on config and templates, update if necessary""" + getSyncedConfig() + getLocalConfig() + getOrCreateModel() + + +# Mask toggle hotkey + + +def onHintHotkey(): + mw.web.eval( + """ + var ioBtn = document.getElementById("io-revl-btn"); + if (ioBtn) {ioBtn.click();} + """ + ) + + +def on_mw_state_shortcuts(state: str, shortcuts: list): + """Add hint hotkey when in Reviewer""" + if state != "review": + return + shortcuts.append(("G", onHintHotkey)) + + +# Retain scroll position when answering + +# TODO: Handle in JS + + +def onShowAnswer(self, _old): + """Retain scroll position across answering the card""" + if not self.card or not self.card.model()["name"] == IO_MODEL_NAME: + return _old(self) + scroll_pos = self.web.page().scrollPosition() + ret = _old(self) + self.web.eval("window.scrollTo({}, {});".format(scroll_pos.x(), scroll_pos.y())) + return ret + + +def setup_menus(): + options_action = QAction(_("Image &Occlusion Enhanced Options..."), mw) + help_action = QAction(_("Image &Occlusion Enhanced..."), mw) + options_action.triggered.connect(onIoSettings) + mw.addonManager.setConfigAction(__name__, onIoSettings) + help_action.triggered.connect(onIoHelp) + mw.form.menuTools.addAction(options_action) + mw.form.menuHelp.addAction(help_action) + + +def setup_main(): + setup_menus() + + # Set up hooks and monkey patches + + # Add-on setup at main window load time + + try: # 2.1.28+ + from aqt.gui_hooks import main_window_did_init + + main_window_did_init.append(on_main_window_did_init) + except (ImportError, ModuleNotFoundError): + try: # 2.1.20+ + from aqt.gui_hooks import profile_did_open + + profile_did_open.append(on_profile_loaded_singleshot) + except (ImportError, ModuleNotFoundError): + addHook("profileLoaded", on_profile_loaded_singleshot) + + # Add-on setup at profile load time + + try: # 2.1.20+ + from aqt.gui_hooks import profile_did_open + + profile_did_open.append(on_profile_loaded) + except (ImportError, ModuleNotFoundError): + addHook("profileLoaded", on_profile_loaded) + + # aqt.editor.Editor + + try: # 2.1.20+ + from aqt.gui_hooks import editor_did_init_buttons + + editor_did_init_buttons.append(onSetupEditorButtons) + except (ImportError, ModuleNotFoundError): + addHook("setupEditorButtons", onSetupEditorButtons) + + try: # 2.1.20+ + from aqt.gui_hooks import editor_will_show_context_menu + + editor_will_show_context_menu.append(maybe_add_image_menu) + except (ImportError, ModuleNotFoundError): + EditorWebView.contextMenuEvent = legacyEditorContextMenuEvent + + try: # 2.1.20+ + from aqt.gui_hooks import editor_will_load_note + + editor_will_load_note.append(on_editor_will_load_note) + except (ImportError, ModuleNotFoundError): + Editor.setNote = wrap(Editor.setNote, legacyOnSetNote, "after") + + Editor.onImgOccButton = onImgOccButton + + # aqt.reviewer.Reviewer + + Reviewer._showAnswer = wrap(Reviewer._showAnswer, onShowAnswer, "around") + + try: # 2.1.20+ + from aqt.gui_hooks import state_shortcuts_will_change + + state_shortcuts_will_change.append(on_mw_state_shortcuts) + except (ImportError, ModuleNotFoundError): + addHook( + "reviewStateShortcuts", + lambda shortcuts: on_mw_state_shortcuts("review", shortcuts), + ) diff --git a/src/image_occlusion_enhanced/nconvert.py b/src/image_occlusion_enhanced/nconvert.py new file mode 100644 index 00000000..b9f33def --- /dev/null +++ b/src/image_occlusion_enhanced/nconvert.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Makes older IO notes editable. +""" + +import logging + +from aqt.qt import * +from anki.hooks import addHook +from aqt.utils import tooltip +from .lang import _ + +from xml.dom import minidom + +from .config import * +from .dialogs import ioAskUser +from .utils import img2path, fname2img + + +class ImgOccNoteConverter(object): + def __init__(self, browser): + self.browser = browser + self.occl_id_last = None + loadConfig(self) + + def convertNotes(self, nids): + """Main note conversion method""" + nids_by_nr = {} + skipped = 0 + (io_nids, filtered) = self.filterSelected(nids) + for nid in io_nids: + note = mw.col.getNote(nid) + (uniq_id, note_nr) = self.getDataFromNamingScheme(note) + if uniq_id == False: + logging.debug(_("Skipping note that couldn't be parsed: %s"), nid) + skipped += 1 + continue + occl_tp = self.getOcclTypeAndNodes(note) + occl_id = uniq_id + "-" + occl_tp + if occl_id == self.occl_id_last: + logging.debug(_("Skipping note that we've just converted: %s"), nid) + continue + self.occl_id_last = occl_id + for nid in self.findByNoteId(uniq_id): + note = mw.col.getNote(nid) + (uniq_id, note_nr) = self.getDataFromNamingScheme(note) + if uniq_id == False: + logging.debug(_("Skipping note that couldn't be parsed: %s"), nid) + skipped += 1 + continue + nids_by_nr[int(note_nr)] = nid + self.idAndCorrelateNotes(nids_by_nr, occl_id) + converted = len(io_nids) + tooltip( + _( + "{number_notes_updated} note(s) updated," + " {number_notes_skipped} skipped" + ).format( + number_notes_updated=converted - skipped, + number_notes_skipped=filtered + skipped, + ) + ) + + def filterSelected(self, nids): + """Filters out notes with the wrong note type and those that are + valid already""" + io_nids = [] + filtered = 0 + for nid in nids: + note = mw.col.getNote(nid) + if note.model() != self.model: + logging.debug(_("Skipping note with wrong note type: %s"), nid) + filtered += 1 + continue + elif note[self.ioflds["id"]]: + logging.debug(_("Skipping IO note that is already editable: %s"), nid) + filtered += 1 + continue + elif not note[self.ioflds["om"]]: + logging.debug(_("Skipping IO note without original SVG mask: %s"), nid) + filtered += 1 + continue + logging.debug(_("Found IO note in need of update: %s"), nid) + io_nids.append(nid) + return (io_nids, filtered) + + def findByNoteId(self, note_id): + """Search collection for notes with given ID in their omask paths""" + # need to use omask path because Note ID field is not yet set + query = '"%s:*%s*"' % (self.ioflds["om"], note_id) + logging.debug("query: %s", query) + res = mw.col.findNotes(query) + return res + + def getDataFromNamingScheme(self, note): + """Get unique ID and note nr from qmask path""" + qmask = note[self.ioflds["qm"]] + path = img2path(qmask, True) + if not path: + return (False, None) + grps = path.split("_") + try: + if len(grps) == 2: + logging.debug(_("Extracting data using IO 2.0 naming scheme")) + uniq_id = grps[0] + note_nr = path.split(" ")[1].split(".")[0] + else: + logging.debug(_("Extracting data using IO Enhanced naming scheme")) + grps = path.split("-") + uniq_id = grps[0] + note_nr = int(grps[2]) - 1 + return (uniq_id, note_nr) + except IndexError: + return (False, None) + + def idAndCorrelateNotes(self, nids_by_nr, occl_id): + """Update Note ID fields and omasks of all occlusion session siblings""" + logging.debug("occl_id %s", occl_id) + logging.debug("nids_by_nr %s", nids_by_nr) + logging.debug("mnode_idxs %s", self.mnode_idxs) + + for nr in sorted(nids_by_nr.keys()): + try: + midx = self.mnode_idxs[nr] + except IndexError: + continue + nid = nids_by_nr[nr] + note = mw.col.getNote(nid) + new_mnode_id = occl_id + "-" + str(nr + 1) + self.mnode.childNodes[midx].setAttribute("id", new_mnode_id) + note[self.ioflds["id"]] = new_mnode_id + note.flush() + logging.debug(_("Adding ID for note nr %s"), nr) + logging.debug("midx %s", midx) + logging.debug("nid %s", nid) + logging.debug("note %s", note) + logging.debug("new_mnode_id %s", new_mnode_id) + + new_svg = self.svg_node.toxml() + omask_path = self._saveMask(new_svg, occl_id, "O") + logging.debug("omask_path %s", omask_path) + + for nid in list(nids_by_nr.values()): + note = mw.col.getNote(nid) + note[self.ioflds["om"]] = fname2img(omask_path) + note.addTag(".io-converted") + note.flush() + logging.debug(_("Setting om and tag for nid %s"), nid) + + def getOcclTypeAndNodes(self, note): + """Determine oclusion type and svg mask nodes""" + nr_of_masks = {} + mnode_idxs = {} + svg_mlayer = {} + for i in ["qm", "om"]: # om second, so that end vars are correct + svg_file = img2path(note[self.ioflds[i]], True) + svg_node = self.readSvg(svg_file) + svg_mlayer = self.layerNodesFrom(svg_node)[-1] # topmost layer + mnode_idxs = self.getMaskNodes(svg_mlayer) + nr_of_masks[i] = len(mnode_idxs) + # decide on occl_tp based on nr of mask nodes in omask vs qmask + if nr_of_masks["om"] != nr_of_masks["qm"]: + occl_tp = "oa" + else: + occl_tp = "ao" + self.svg_node = svg_node + self.mnode = svg_mlayer + self.mnode_idxs = mnode_idxs + return occl_tp + + def readSvg(self, svg_file): + """Read and fix malformatted IO 2.0 SVGs""" + svg_doc = minidom.parse(svg_file) + # ugly workaround for wrong namespace in older IO notes: + svg_string = svg_doc.toxml().replace("ns0:", "").replace(":ns0", "") + svg_string = str(svg_string) + svg_doc = minidom.parseString(svg_string.encode("utf-8")) + svg_node = svg_doc.documentElement + return svg_node + + def getMaskNodes(self, mlayer): + """Find mask nodes in masks layer""" + mnode_indexes = [] + for i, node in enumerate(mlayer.childNodes): + if (node.nodeType == node.ELEMENT_NODE) and (node.nodeName != "title"): + mnode_indexes.append(i) + return mnode_indexes + + def layerNodesFrom(self, svg_node): + """Get layer nodes (topmost group nodes below the SVG node)""" + assert svg_node.nodeType == svg_node.ELEMENT_NODE + assert svg_node.nodeName == "svg" + layer_nodes = [ + node for node in svg_node.childNodes if node.nodeType == node.ELEMENT_NODE + ] + assert len(layer_nodes) >= 1 + # last, i.e. top-most element, needs to be a layer: + assert layer_nodes[-1].nodeName == "g" + return layer_nodes + + def _saveMask(self, mask, note_id, mtype): + """Write mask to file in media collection""" + logging.debug( + _("!saving %(node_id)s, %(mtype)s"), {"node_id": node_id, "mtype": mtype} + ) + mask_path = "%s-%s.svg" % (note_id, mtype) + mask_file = open(mask_path, "w") + mask_file.write(mask.encode("utf-8")) + mask_file.close() + return mask_path + + +def onIoConvert(self): + """Launch initial dialog, set up checkpoint, invoke converter""" + mw = self.mw + selected = self.selectedNotes() + if not selected: + tooltip(_("No cards selected."), period=2000) + return + ret = ioAskUser( + "question_nconvert", + title=_("Please confirm action"), + parent=self, + defaultno=True, + ) + if not ret: + return False + mw.progress.start() + mw.checkpoint(_("Image Occlusion Note Conversions")) + self.model.beginReset() + conv = ImgOccNoteConverter(self) + conv.convertNotes(selected) + self.model.endReset() + mw.col.reset() + mw.reset() + mw.progress.finish() + + +# Set up menus and hooks + + +def setupMenu(self): + menu = self.form.menuEdit + menu.addSeparator() + a = menu.addAction(_("Convert to Editable IO &Enhanced Notes")) + a.triggered.connect(lambda _, b=self: onIoConvert(b)) + + +try: + from aqt.gui_hooks import browser_menus_did_init + + browser_menus_did_init.append(setupMenu) +except (ImportError, ModuleNotFoundError): + addHook("browser.setupMenus", setupMenu) diff --git a/src/image_occlusion_enhanced/ngen.py b/src/image_occlusion_enhanced/ngen.py new file mode 100644 index 00000000..c5d592d9 --- /dev/null +++ b/src/image_occlusion_enhanced/ngen.py @@ -0,0 +1,554 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# Copyright (C) 2013 Steve AW +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Generates the actual IO notes and writes them to +the collection. +""" + +import logging + +from aqt.qt import * +from aqt import mw +from aqt.utils import tooltip +from anki.notes import Note + +from xml.dom import minidom +import uuid + +from .dialogs import ioAskUser +from .utils import fname2img +from .config import * +from .lang import _, ngettext + +# Explanation of some of the variables: +# +# nid: Note ID set by Anki +# note_id: Image Occlusion Note ID set as the first field of each IO note +# uniq_id: Unique sequence of random characters. First part of the note_id +# occl_tp: Two-letter code that signifies occlusion type. Second part of +# the note_id +# occl_id: Combination of uniq_id + occl_tp - unique identifier shared +# by all notes created in one IO session +# note_nr: Third part of the note_id + + +def genByKey(key, old_occl_tp=None): + """Get note generator based on occl_tp/user input""" + if key in ["Don't Change"]: + return genByKey(old_occl_tp, None) + elif key in ["ao", "Hide All, Guess One"]: + return IoGenHideAllRevealOne + elif key in ["oa", "Hide One, Guess One"]: + return IoGenHideOneRevealAll + else: + return IoGenHideAllRevealOne + + +class ImgOccNoteGenerator(object): + """Generic note generator object""" + + stripattr = ["opacity", "stroke-opacity", "fill-opacity"] + + def __init__(self, ed, svg, image_path, opref, tags, fields, did): + self.ed = ed + self.new_svg = svg + self.image_path = image_path + self.opref = opref + self.tags = tags + self.fields = fields + self.did = did + self.qfill = "#" + mw.col.conf["imgocc"]["qfill"] + loadConfig(self) + + def generateNotes(self): + """Generate new notes""" + state = "default" + self.uniq_id = str(uuid.uuid4()).replace("-", "") + self.occl_id = "%s-%s" % (self.uniq_id, self.occl_tp) + + (svg_node, layer_node) = self._getMnodesAndSetIds() + if not self.mnode_ids: + tooltip( + _( + "No cards to generate.
      " + "Are you sure you set your masks correctly?" + ) + ) + return False + + self.new_svg = svg_node.toxml() # write changes to svg + omask_path = self._saveMask(self.new_svg, self.occl_id, "O") + qmasks = self._generateMaskSVGsFor("Q") + amasks = self._generateMaskSVGsFor("A") + image_path = mw.col.media.addFile(self.image_path) + img = fname2img(image_path) + + mw.checkpoint(_("Adding Image Occlusion Cards")) + for nr, idx in enumerate(self.mnode_indexes): + note_id = self.mnode_ids[idx] + self._saveMaskAndReturnNote( + omask_path, qmasks[nr], amasks[nr], img, note_id + ) + tooltip( + ngettext( + "One card added", "{card_count} cards added", len(qmasks) + ).format(card_count=len(qmasks)), + parent=None, + ) + return state + + def updateNotes(self): + """Update existing notes""" + state = "default" + self.uniq_id = self.opref["uniq_id"] + self.occl_id = "%s-%s" % (self.uniq_id, self.occl_tp) + omask_path = None + + self._findAllNotes() + (svg_node, mlayer_node) = self._getMnodesAndSetIds(True) + if not self.mnode_ids: + tooltip( + _( + "No shapes left. You can't delete all cards.
      " + "Are you sure you set your masks correctly?" + ) + ) + return False + mw.checkpoint(_("Editing Image Occlusion Cards")) + ret = self._deleteAndIdNotes(mlayer_node) + if not ret: + # confirmation window rejected + return False + else: + (del_count, new_count) = ret + + self.new_svg = svg_node.toxml() # write changes to svg + old_svg = self._getOriginalSvg() # load original svg + if self.new_svg != old_svg or self.occl_tp != self.opref["occl_tp"]: + # updated masks or occlusion type + omask_path = self._saveMask(self.new_svg, self.occl_id, "O") + qmasks = self._generateMaskSVGsFor("Q") + amasks = self._generateMaskSVGsFor("A") + state = "reset" + + image_path = mw.col.media.addFile(self.image_path) + img = fname2img(image_path) + + logging.debug("mnode_indexes %s", self.mnode_indexes) + for nr, idx in enumerate(self.mnode_indexes): + logging.debug("=====================") + logging.debug("nr %s", nr) + logging.debug("idx %s", idx) + note_id = self.mnode_ids[idx] + logging.debug("note_id %s", note_id) + logging.debug("self.nids %s", self.nids) + nid = self.nids[note_id] + logging.debug("nid %s", nid) + if omask_path: + self._saveMaskAndReturnNote( + omask_path, qmasks[nr], amasks[nr], img, note_id, nid + ) + else: + self._saveMaskAndReturnNote(None, None, None, img, note_id, nid) + self._showUpdateTooltip(del_count, new_count) + return state + + def _showUpdateTooltip(self, del_count, new_count): + upd_count = max(0, len(self.mnode_indexes) - del_count - new_count) + ttip = ngettext( + "One old card edited in place", + "{card_count} old cards edited in place", + upd_count, + ).format(card_count=upd_count) + if del_count > 0: + ttip += ngettext( + "
      One existing card deleted", + "
      {card_count} existing cards deleted", + upd_count, + ).format(card_count=del_count) + if new_count > 0: + ttip += ngettext( + "
      One new card created", + "
      {card_count} new cards created", + new_count, + ).format(card_count=new_count) + tooltip(ttip, parent=self.ed.parentWindow) + + def _getOriginalSvg(self): + """Returns original SVG as a string""" + mask_doc = minidom.parse(self.opref["omask"]) + svg_node = mask_doc.documentElement + return svg_node.toxml() + + def _layerNodesFrom(self, svg_node): + """Get layer nodes (topmost group nodes below the SVG node)""" + assert svg_node.nodeType == svg_node.ELEMENT_NODE + assert svg_node.nodeName == "svg" + layer_nodes = [ + node for node in svg_node.childNodes if node.nodeType == node.ELEMENT_NODE + ] + assert len(layer_nodes) >= 1 + # last, i.e. top-most element, needs to be a layer: + assert layer_nodes[-1].nodeName == "g" + return layer_nodes + + def _getMnodesAndSetIds(self, edit=False): + """Find mask nodes in masks layer and read/set node IDs""" + self.mnode_indexes = [] + self.mnode_ids = {} + mask_doc = minidom.parseString(self.new_svg.encode("utf-8")) + svg_node = mask_doc.documentElement + cheight = float(svg_node.attributes["height"].value) + cwidth = float(svg_node.attributes["width"].value) + carea = cheight * cwidth + layer_nodes = self._layerNodesFrom(svg_node) + mlayer_node = layer_nodes[-1] # treat topmost layer as masks layer + + shift = 0 + for i, mnode in enumerate(mlayer_node.childNodes): + # minidom doesn't offer a childElements method and childNodes + # also returns whitespace found in the mlayer_node as a child node. + # For that reason we use self.mnode_indexes to register all + # indexes of mlayer_node children that contain actual elements, + # i.e. mask nodes + if (mnode.nodeType == mnode.ELEMENT_NODE) and (mnode.nodeName != "title"): + i -= shift + if not edit and mnode.nodeName == "rect": + # remove microscopical shapes (usually accidentally drawn) + h_attr = mnode.attributes.get("height", 0) + w_attr = mnode.attributes.get("width", 0) + height = ( + h_attr + if not h_attr + else float(mnode.attributes["height"].value) + ) + width = ( + w_attr if not w_attr else float(mnode.attributes["width"].value) + ) + if ( + not height + or not width + or 100 * (height * width) / carea <= 0.01 + ): + mlayer_node.removeChild(mnode) + shift += 1 + continue + self.mnode_indexes.append(i) + self._removeAttribsRecursively(mnode, self.stripattr) + if mnode.nodeName == "g": + # remove IDs of grouped shapes to prevent duplicates down the line + for node in mnode.childNodes: + self._removeAttribsRecursively(node, ["id"]) + if not edit: + self.mnode_ids[i] = "%s-%i" % ( + self.occl_id, + len(self.mnode_indexes), + ) + mnode.setAttribute("id", self.mnode_ids[i]) + else: + self.mnode_ids[i] = mnode.attributes["id"].value + + return (svg_node, mlayer_node) + + def _findByNoteId(self, note_id): + """Search collection for notes with given ID""" + query = '"%s:%s*"' % (self.ioflds["id"], note_id) + logging.debug("query %s", query) + res = mw.col.findNotes(query) + return res + + def _findAllNotes(self): + """Get matching nids by ID""" + old_occl_id = "%s-%s" % (self.uniq_id, self.opref["occl_tp"]) + res = self._findByNoteId(old_occl_id) + self.nids = {} + for nid in res: + note_id = mw.col.getNote(nid)[self.ioflds["id"]] + self.nids[note_id] = nid + logging.debug("--------------------") + logging.debug("res %s", res) + logging.debug("nids %s", self.nids) + + def _deleteAndIdNotes(self, mlayer_node): + """ + Determine which mask nodes have been deleted or newly created and, depending + on which, either delete their respective notes or ID them in correspondence + with the numbering of older nodes + """ + uniq_id = self.opref["uniq_id"] + mnode_ids = self.mnode_ids + nids = self.nids + + # look for missing shapes by note_id + valid_mnode_note_ids = [ + x for x in list(mnode_ids.values()) if x.startswith(uniq_id) + ] + valid_nid_note_ids = [x for x in list(nids.keys()) if x.startswith(uniq_id)] + # filter out notes that have already been deleted manually + exstg_mnode_note_ids = [ + x for x in valid_mnode_note_ids if x in valid_nid_note_ids + ] + exstg_mnode_note_nrs = sorted( + [int(i.split("-")[-1]) for i in exstg_mnode_note_ids] + ) + # determine available nrs available for note numbering + if not exstg_mnode_note_nrs: + # only the case if the user deletes all existing shapes + max_mnode_note_nr = 0 + full_range = None + available_nrs = None + else: + max_mnode_note_nr = int(exstg_mnode_note_nrs[-1]) + full_range = list(range(1, max_mnode_note_nr + 1)) + available_nrs = set(full_range) - set(exstg_mnode_note_nrs) + available_nrs = sorted(list(available_nrs)) + + # compare note_ids as present in note collection with masks on svg + deleted_note_ids = set(valid_nid_note_ids) - set(valid_mnode_note_ids) + deleted_note_ids = sorted(list(deleted_note_ids)) + del_count = len(deleted_note_ids) + # set notes of missing masks on svg to be deleted + deleted_nids = [nids[x] for x in deleted_note_ids] + + logging.debug("--------------------") + logging.debug("valid_mnode_note_ids %s", valid_mnode_note_ids) + logging.debug("exstg_mnode_note_nrs %s", exstg_mnode_note_nrs) + logging.debug("max_mnode_note_nr %s", max_mnode_note_nr) + logging.debug("full_range %s", full_range) + logging.debug("available_nrs %s", available_nrs) + logging.debug("--------------------") + logging.debug("valid_nid_note_ids %s", valid_nid_note_ids) + logging.debug("deleted_note_ids %s", deleted_note_ids) + logging.debug("deleted_nids %s", deleted_nids) + + # add note_id to missing shapes + note_nr_max = max_mnode_note_nr + new_count = 0 + for nr, idx in enumerate(self.mnode_indexes): + mnode_id = mnode_ids[idx] + new_mnode_id = None + mnode = mlayer_node.childNodes[idx] + if mnode_id not in exstg_mnode_note_ids: + if available_nrs: + # use gap in note_id numbering + note_nr = available_nrs.pop(0) + else: + # increment maximum note_id number + note_nr_max = note_nr_max + 1 + note_nr = note_nr_max + new_mnode_id = self.occl_id + "-" + str(note_nr) + new_count += 1 + nids[new_mnode_id] = None + else: + # update occlusion type + mnode_id_nr = mnode_id.split("-")[-1] + new_mnode_id = self.occl_id + "-" + mnode_id_nr + nids[new_mnode_id] = nids.pop(mnode_id) + if new_mnode_id: + mnode.setAttribute("id", new_mnode_id) + self.mnode_ids[idx] = new_mnode_id + + logging.debug("=====================") + logging.debug("nr %s", nr) + logging.debug("idx %s", idx) + logging.debug("mnode_id %s", mnode_id) + logging.debug("available_nrs %s", available_nrs) + logging.debug("note_nr_max %s", note_nr_max) + logging.debug("new_mnode_id %s", new_mnode_id) + + logging.debug("--------------------") + logging.debug("edited nids %s", nids) + logging.debug("edited self.mnode_ids %s", self.mnode_ids) + + if del_count or new_count: + q = _( + "This will delete {del_count} card(s) and " + "create {new_count} new one(s). " + "Please note that this action is irreversible.

      " + "Would you still like to proceed?" + ).format(del_count=del_count, new_count=new_count) + if not ioAskUser( + "custom", + text=q, + title=_("Please confirm action"), + parent=self.ed.imgoccadd.imgoccedit, + help="edit", + ): + # TODO: pass imgoccedit instance to ngen in order to avoid ↑ this + return False + + if deleted_nids: + mw.col.remNotes(deleted_nids) + return (del_count, new_count) + + def _generateMaskSVGsFor(self, side): + """Generate a mask for each mask node""" + masks = [ + self._createMask(side, node_index) for node_index in self.mnode_indexes + ] + return masks + + def _createMask(self, side, mask_node_index): + """Call occl_tp-specific mask generator""" + mask_doc = minidom.parseString(self.new_svg.encode("utf-8")) + svg_node = mask_doc.documentElement + layer_nodes = self._layerNodesFrom(svg_node) + mlayer_node = layer_nodes[-1] # treat topmost layer as masks layer + # This method gets implemented differently by subclasses + self._createMaskAtLayernode(side, mask_node_index, mlayer_node) + return svg_node.toxml() + + def _createMaskAtLayernode(self, mask_node_index, mlayer_node): + raise NotImplementedError + + def _setQuestionAttribs(self, node): + """Set question node color and class""" + if node.nodeType == node.ELEMENT_NODE and node.tagName != "text": + # set question class + node.setAttribute("class", "qshape") + if node.hasAttribute("fill"): + # set question color + node.setAttribute("fill", self.qfill) + list(map(self._setQuestionAttribs, node.childNodes)) + + def _removeAttribsRecursively(self, node, attrs): + """Remove provided attributes recursively from node and children""" + if node.nodeType == node.ELEMENT_NODE: + for i in attrs: + if node.hasAttribute(i): + node.removeAttribute(i) + for i in node.childNodes: + self._removeAttribsRecursively(i, attrs) + + def _saveMask(self, mask, note_id, mtype): + """Write mask to file in media collection""" + logging.debug( + _("!saving %(note_id)s, %(mtype)s"), {"note_id": note_id, "mtype": mtype} + ) + # media collection is the working directory: + mask_path = "%s-%s.svg" % (note_id, mtype) + mask_file = open(mask_path, "wb") + mask_file.write(mask.encode("utf8")) + mask_file.close() + return mask_path + + def removeBlanks(self, node): + for x in node.childNodes: + if x.nodeType == node.TEXT_NODE: + if x.nodeValue: + x.nodeValue = x.nodeValue.strip() + elif x.nodeType == node.ELEMENT_NODE: + self.removeBlanks(x) + + def _saveMaskAndReturnNote(self, omask_path, qmask, amask, img, note_id, nid=None): + """Write actual note for given qmask and amask""" + fields = self.fields + model = self.model + mflds = self.mflds + fields[self.ioflds["im"]] = img + if omask_path: + # Occlusions updated + qmask_path = self._saveMask(qmask, note_id, "Q") + amask_path = self._saveMask(amask, note_id, "A") + fields[self.ioflds["qm"]] = fname2img(qmask_path) + fields[self.ioflds["am"]] = fname2img(amask_path) + fields[self.ioflds["om"]] = fname2img(omask_path) + fields[self.ioflds["id"]] = note_id + + self.model["did"] = self.did + if nid: + note = mw.col.getNote(nid) + else: + note = Note(mw.col, model) + + # add fields to note + note.tags = self.tags + for i in mflds: + fname = i["name"] + if fname in fields: + # only update fields that have been modified + note[fname] = fields[fname] + + if nid: + note.flush() + logging.debug(_("!noteflush %s"), note) + else: + mw.col.addNote(note) + logging.debug(_("!notecreate %s"), note) + + +# Different generator subclasses for different occlusion types: + + +class IoGenHideAllRevealOne(ImgOccNoteGenerator): + """ + Q: All hidden, one prompted for. A: One revealed + ('nonoverlapping' / "Hide all, guess one") + """ + + occl_tp = "ao" + + def __init__(self, ed, svg, image_path, opref, tags, fields, did): + ImgOccNoteGenerator.__init__( + self, ed, svg, image_path, opref, tags, fields, did + ) + + def _createMaskAtLayernode(self, side, mask_node_index, mlayer_node): + mask_node = mlayer_node.childNodes[mask_node_index] + if side == "Q": + self._setQuestionAttribs(mask_node) + elif side == "A": + mlayer_node.removeChild(mask_node) + + +class IoGenHideOneRevealAll(ImgOccNoteGenerator): + """ + Q: One hidden, one prompted for. A: All revealed + ("overlapping" / "Hide one, guess one") + """ + + occl_tp = "oa" + + def __init__(self, ed, svg, image_path, opref, tags, fields, did): + ImgOccNoteGenerator.__init__( + self, ed, svg, image_path, opref, tags, fields, did + ) + + def _createMaskAtLayernode(self, side, mask_node_index, mlayer_node): + for i in reversed(self.mnode_indexes): + mask_node = mlayer_node.childNodes[i] + if i == mask_node_index and side == "Q": + self._setQuestionAttribs(mask_node) + mask_node.setAttribute("class", "qshape") + else: + mlayer_node.removeChild(mask_node) diff --git a/src/image_occlusion_enhanced/options.py b/src/image_occlusion_enhanced/options.py new file mode 100644 index 00000000..54d64c9c --- /dev/null +++ b/src/image_occlusion_enhanced/options.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- + +# Image Occlusion Enhanced Add-on for Anki +# +# Copyright (C) 2016-2020 Aristotelis P. +# Copyright (C) 2012-2015 Tiago Barroso +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Main options dialog +""" + +import logging + +from aqt.qt import * +from aqt.utils import showInfo + +from aqt import mw +from anki.errors import AnkiError + +from .config import * +from .lang import _ + + +class GrabKey(QDialog): + """ + Grab the key combination to paste the resized image + + Largely based on ImageResizer by searene + (https://github.com/searene/Anki-Addons) + """ + + def __init__(self, parent): + QDialog.__init__(self, parent=parent) + self.parent = parent + self.key = parent.hotkey + # self.active is used to trace whether there's any key held now + self.active = 0 + self.ctrl = False + self.alt = False + self.shift = False + self.extra = None + self.setupUI() + + def setupUI(self): + mainLayout = QVBoxLayout() + self.setLayout(mainLayout) + + label = QLabel(_("Please press the new key combination")) + mainLayout.addWidget(label) + + self.setWindowTitle(_("Grab key combination")) + + def keyPressEvent(self, evt): + self.active += 1 + if evt.key() > 0 and evt.key() < 127: + self.extra = chr(evt.key()) + elif evt.key() == Qt.Key_Control: + self.ctrl = True + elif evt.key() == Qt.Key_Alt: + self.alt = True + elif evt.key() == Qt.Key_Shift: + self.shift = True + + def keyReleaseEvent(self, evt): + self.active -= 1 + + if self.active != 0: + return + if not (self.shift or self.ctrl or self.alt): + showInfo( + _("Please use at least one keyboard " "modifier (Ctrl, Alt, Shift)") + ) + return + if self.shift and not (self.ctrl or self.alt): + showInfo( + _( + "Shift needs to be combined with at " + "least one other modifier (Ctrl, Alt)" + ) + ) + return + if not self.extra: + showInfo( + _( + "Please press at least one key " + "that is not a keyboard modifier (not Ctrl/Alt/Shift)" + ) + ) + return + + combo = [] + if self.ctrl: + combo.append("Ctrl") + if self.shift: + combo.append("Shift") + if self.alt: + combo.append("Alt") + combo.append(self.extra) + + self.parent.updateHotkey("+".join(combo)) + self.close() + + +class ImgOccOpts(QDialog): + """Main Image Occlusion Options dialog""" + + def __init__(self): + QDialog.__init__(self, parent=mw) + loadConfig(self) + self.ofill = self.sconf["ofill"] + self.qfill = self.sconf["qfill"] + self.scol = self.sconf["scol"] + self.swidth = self.sconf["swidth"] + self.font = self.sconf["font"] + self.fsize = self.sconf["fsize"] + self.hotkey = self.lconf["hotkey"] + self.setupUi() + self.setupValues(self.sconf) + + def setupValues(self, config): + """Set up widget data based on provided config dict""" + self.updateHotkey() + self.changeButtonColor(self.ofill_btn, config["ofill"]) + self.changeButtonColor(self.qfill_btn, config["qfill"]) + self.changeButtonColor(self.scol_btn, config["scol"]) + self.swidth_sel.setValue(int(config["swidth"])) + self.fsize_sel.setValue(int(config["fsize"])) + self.swidth_sel.setValue(int(config["swidth"])) + self.font_sel.setCurrentFont(QFont(config["font"])) + self.skipped.setText(",".join(config["skip"])) + + def setupUi(self): + """Set up widgets and layouts""" + + # Top section + qfill_label = QLabel(_("Question mask")) + ofill_label = QLabel(_("Other masks")) + scol_label = QLabel(_("Lines")) + colors_heading = QLabel(_("Colors")) + fields_heading = QLabel(_("Custom Field Names")) + other_heading = QLabel(_("Other Editor Settings")) + + self.qfill_btn = QPushButton() + self.ofill_btn = QPushButton() + self.scol_btn = QPushButton() + self.qfill_btn.clicked.connect( + lambda _, t="qfill", b=self.qfill_btn: self.getNewColor(t, b) + ) + self.ofill_btn.clicked.connect( + lambda _, t="ofill", b=self.ofill_btn: self.getNewColor(t, b) + ) + self.scol_btn.clicked.connect( + lambda _, t="scol", b=self.scol_btn: self.getNewColor(t, b) + ) + + swidth_label = QLabel(_("Line width")) + font_label = QLabel(_("Label font")) + fsize_label = QLabel(_("Label size")) + + self.swidth_sel = QSpinBox() + self.swidth_sel.setMinimum(0) + self.swidth_sel.setMaximum(20) + self.font_sel = QFontComboBox() + self.fsize_sel = QSpinBox() + self.fsize_sel.setMinimum(5) + self.fsize_sel.setMaximum(300) + + # Horizontal lines + rule1 = self.create_horizontal_rule() + rule2 = self.create_horizontal_rule() + + # Bottom section and grid assignment + + fields_text = _( + "Changing any of the entries below will rename " + "the corresponding default field of the IO Enhanced " + "note type. This is the only way you can rename any " + "of the default fields.

      Renaming these " + "fields through Anki's regular dialogs will cause " + "the add-on to fail. So please don't do that." + ) + + fields_description = QLabel(fields_text) + fields_description.setWordWrap(True) + + grid = QGridLayout() + grid.setSpacing(10) + + grid.addWidget(colors_heading, 0, 0, 1, 3) + grid.addWidget(qfill_label, 1, 0, 1, 1) + grid.addWidget(self.qfill_btn, 1, 1, 1, 2) + grid.addWidget(ofill_label, 2, 0, 1, 1) + grid.addWidget(self.ofill_btn, 2, 1, 1, 2) + grid.addWidget(scol_label, 3, 0, 1, 1) + grid.addWidget(self.scol_btn, 3, 1, 1, 2) + + grid.addWidget(other_heading, 0, 3, 1, 3) + grid.addWidget(swidth_label, 1, 3, 1, 1) + grid.addWidget(self.swidth_sel, 1, 4, 1, 2) + grid.addWidget(font_label, 2, 3, 1, 1) + grid.addWidget(self.font_sel, 2, 4, 1, 2) + grid.addWidget(fsize_label, 3, 3, 1, 1) + grid.addWidget(self.fsize_sel, 3, 4, 1, 2) + + grid.addWidget(rule1, 4, 0, 1, 6) + grid.addWidget(fields_heading, 5, 0, 1, 6) + grid.addWidget(fields_description, 6, 0, 1, 6) + + # Field name entries + row = 7 + clm = 0 + self.lnedit = {} + for key in IO_FLDS_IDS: + if row == 13: # switch to right columns + clm = 3 + row = 7 + default_name = self.sconf_dflt["flds"][key] + current_name = self.sconf["flds"][key] + lb = QLabel(default_name) + lb.setTextInteractionFlags(Qt.TextSelectableByMouse) + t = QLineEdit() + t.setText(current_name) + grid.addWidget(lb, row, clm, 1, 2) + grid.addWidget(t, row, clm + 1, 1, 2) + self.lnedit[key] = t + row += 1 + + # Misc settings + misc_heading = QLabel(_("Miscellaneous Settings")) + + # Skipped fields: + skipped_description = QLabel( + _( + "Comma-separated list of " + "fields to hide in Editing mode " + "(in order to preserve manual edits):" + ) + ) + self.skipped = QLineEdit() + + # Hotkey: + key_grab_label = QLabel(_("Invoke IO with the following hotkey:")) + self.key_grabbed = QLabel("") + key_grab_btn = QPushButton(_("Change hotkey"), self) + key_grab_btn.clicked.connect(self.showGrabKey) + + grid.addWidget(rule2, row + 1, 0, 1, 6) + grid.addWidget(misc_heading, row + 2, 0, 1, 6) + grid.addWidget(skipped_description, row + 3, 0, 1, 6) + grid.addWidget(self.skipped, row + 4, 0, 1, 6) + grid.addWidget(key_grab_label, row + 5, 0, 1, 2) + grid.addWidget(self.key_grabbed, row + 5, 2, 1, 1) + grid.addWidget(key_grab_btn, row + 5, 3, 1, 3) + + # Main button box + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + defaults_btn = button_box.addButton( + _("Restore &Defaults"), QDialogButtonBox.ResetRole + ) + defaults_btn.clicked.connect(self.restoreDefaults) + button_box.accepted.connect(self.onAccept) + button_box.rejected.connect(self.onReject) + + # Main layout + l_main = QVBoxLayout() + l_main.addLayout(grid) + l_main.addWidget(button_box) + self.setLayout(l_main) + self.setMinimumWidth(800) + self.setMinimumHeight(640) + self.setWindowTitle(_("Image Occlusion Enhanced Options")) + + def create_horizontal_rule(self): + """ + Returns a QFrame that is a sunken, horizontal rule. + """ + frame = QFrame() + frame.setFrameShape(QFrame.HLine) + frame.setFrameShadow(QFrame.Sunken) + return frame + + def updateHotkey(self, combo=None): + """Update hotkey label and attribute""" + key = combo or self.hotkey + label = "{}".format(key) + self.key_grabbed.setText(label) + if combo: + self.hotkey = combo + + def showGrabKey(self): + """Invoke key grabber""" + win = GrabKey(self) + win.exec_() + + def getNewColor(self, clrvar, clrbtn): + """Set color via color selection dialog""" + dialog = QColorDialog() + color = dialog.getColor() + if color.isValid(): + # Remove the # sign from QColor.name(): + color = color.name()[1:] + if clrvar == "qfill": + self.qfill = color + elif clrvar == "ofill": + self.ofill = color + elif clrvar == "scol": + self.scol = color + self.changeButtonColor(clrbtn, color) + + def changeButtonColor(self, button, color): + """Generate color preview pixmap and place it on button""" + pixmap = QPixmap(128, 18) + qcolour = QColor(0, 0, 0) + qcolour.setNamedColor("#" + color) + pixmap.fill(qcolour) + button.setIcon(QIcon(pixmap)) + button.setIconSize(QSize(128, 18)) + + def restoreDefaults(self): + """Restore colors and fields back to defaults""" + self.hotkey = self.lconf_dflt["hotkey"] + for key in list(self.lnedit.keys()): + self.lnedit[key].setText(IO_FLDS[key]) + self.lnedit[key].setModified(True) + self.setupValues(self.sconf_dflt) + self.ofill = self.sconf_dflt["ofill"] + self.qfill = self.sconf_dflt["qfill"] + self.scol = self.sconf_dflt["scol"] + + def renameFields(self): + """Check for modified names and rename fields accordingly""" + modified = False + model = getOrCreateModel() + flds = model["flds"] + for key in list(self.lnedit.keys()): + if not self.lnedit[key].isModified(): + continue + name = self.lnedit[key].text() + oldname = mw.col.conf["imgocc"]["flds"][key] + if name is None or not name.strip() or name == oldname: + continue + fnames = mw.col.models.fieldNames(model) + if name in fnames and oldname not in fnames: + # case: imported cards, fields not corresponding to config + mw.col.conf["imgocc"]["flds"][key] = name + modified = True + continue + idx = fnames.index(oldname) + fld = flds[idx] + if fld: + # rename note type fields + mw.col.models.renameField(model, fld, name) + # update imgocc field-id <-> field-name assignment + mw.col.conf["imgocc"]["flds"][key] = name + modified = True + logging.debug( + _("Renamed %(old_name)s, %(new_name)s"), + {"old_name": oldname, "new_name": name}, + ) + if modified: + flds = model["flds"] + + return (modified, flds) + + def onAccept(self): + """Apply changes on OK button press""" + modified = False + try: + (modified, flds) = self.renameFields() + except AnkiError: + print(_("Field rename action aborted")) + return + if modified and hasattr(mw, "ImgOccEdit"): + self.resetIoEditor(flds) + mw.col.conf["imgocc"]["ofill"] = self.ofill + mw.col.conf["imgocc"]["qfill"] = self.qfill + mw.col.conf["imgocc"]["scol"] = self.scol + mw.col.conf["imgocc"]["swidth"] = self.swidth_sel.value() + mw.col.conf["imgocc"]["fsize"] = self.fsize_sel.value() + mw.col.conf["imgocc"]["font"] = self.font_sel.currentFont().family() + mw.col.conf["imgocc"]["skip"] = self.skipped.text().split(",") + mw.pm.profile["imgocc"]["hotkey"] = self.hotkey + mw.col.setMod() + self.close() + + def resetIoEditor(self, flds): + """Reset existing instance of IO Editor""" + # TODO: either delete method or refactor into method that updates running + # instance of I/O (we no longer reuse old ImgOccEdit instances) + dialog = mw.ImgOccEdit + loadConfig(dialog) + dialog.resetFields() + dialog.setupFields(flds) + + def onReject(self): + """Dismiss changes on Close button press""" + self.close() diff --git a/src/image_occlusion_enhanced/svg-edit/.babelrc b/src/image_occlusion_enhanced/svg-edit/.babelrc new file mode 100644 index 00000000..ece1d876 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + ["@babel/preset-env"] + ] +} diff --git a/src/image_occlusion_enhanced/svg-edit/.editorconfig b/src/image_occlusion_enhanced/svg-edit/.editorconfig new file mode 100644 index 00000000..ee4e61cb --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/.editorconfig @@ -0,0 +1,16 @@ +; EditorConfig file: https://EditorConfig.org +; Install the "EditorConfig" plugin into your editor to use + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +; [app/public/css/**.styl] +; indent_style = tab +; indent_size = 2 diff --git a/src/image_occlusion_enhanced/svg-edit/.eslintignore b/src/image_occlusion_enhanced/svg-edit/.eslintignore new file mode 100644 index 00000000..6e19ceb9 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/.eslintignore @@ -0,0 +1,31 @@ +node_modules +ignore + +dist +docs/jsdoc + +svgedit-config-es.js +svgedit-config-iife.js +svgedit-custom.css +editor/xdomain-svgedit-config-iife.js + +# Vendor/minified files +editor/jquery.min.js +editor/jquery-ui + +# Previously minified though exporting +editor/jquerybbq + +# Previously minified though exporting +editor/js-hotkeys + +editor/jspdf/jspdf.min.js +editor/jspdf/underscore-min.js + +editor/extensions/mathjax + +editor/external/* +!editor/external/dom-polyfill +editor/external/dom-polyfill/* +!editor/external/dom-polyfill/dom-polyfill.js +!editor/external/dynamic-import-polyfill diff --git a/src/image_occlusion_enhanced/svg-edit/.eslintrc.js b/src/image_occlusion_enhanced/svg-edit/.eslintrc.js new file mode 100644 index 00000000..1a647263 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/.eslintrc.js @@ -0,0 +1,205 @@ +module.exports = { + extends: [ + "ash-nazg/sauron-node", + "plugin:qunit/recommended", "plugin:testcafe/recommended" + ], + parserOptions: { + sourceType: "module" + }, + // Need to make explicit here for processing by jsdoc/check-examples + plugins: ["qunit"], + env: { + browser: true + }, + settings: { + polyfills: [ + "Array.isArray", + "Blob", + "console", + "Date.now", + "document.body", + "document.evaluate", + "document.head", + "document.importNode", + "document.querySelector", "document.querySelectorAll", + "DOMParser", + "Error", + "fetch", + "FileReader", + "history.pushState", + "history.replaceState", + "JSON", + "location.href", + "location.origin", + "MutationObserver", + "Object.assign", "Object.defineProperty", "Object.defineProperties", + "Object.getOwnPropertyDescriptor", + "Object.entries", "Object.keys", "Object.values", + "Promise", + "Set", + "Uint8Array", + "URL", + "window.getComputedStyle", + "window.postMessage", + "window.scrollX", "window.scrollY", + "XMLHttpRequest", + "XMLSerializer" + ], + jsdoc: { + additionalTagNames: { + // In case we need to extend + customTags: [] + }, + augmentsExtendsReplacesDocs: true, + // Todo: Figure out why this is not working and why seem to have to + // disable for all Markdown: + /* + baseConfig: { + rules: { + "no-multi-spaces": "off" + } + } + */ + } + }, + overrides: [ + // Locales have no need for importing outside of SVG-Edit + { + files: [ + "editor/locale/lang.*.js", "editor/extensions/ext-locale/**", + "docs/tutorials/ExtensionDocs.md" + ], + rules: { + "import/no-anonymous-default-export": ["off"] + } + }, + // For extensions, `this` is generally assigned to be the more + // descriptive `svgEditor`; they also have no need for importing outside + // of SVG-Edit + { + files: ["editor/extensions/**/ext-*.js"], + rules: { + "consistent-this": ["error", "svgEditor"], + "import/no-anonymous-default-export": ["off"] + } + }, + // These browser files don't do importing or requiring + { + files: [ + "editor/svgpathseg.js", "editor/touch.js", "editor/typedefs.js", + "editor/redirect-on-no-module-support.js", + "editor/extensions/imagelib/index.js", + "editor/external/dom-polyfill/dom-polyfill.js", + "test/all_tests.js", "screencasts/svgopen2010/script.js", + "opera-widget/handlers.js", + "firefox-extension/handlers.js", + "firefox-extension/content/svg-edit-overlay.js" + ], + rules: { + "import/unambiguous": ["off"] + } + }, + { + files: ['test/browser-bugs/**'], + rules: { + 'no-var': 'off' + } + }, + { + files: ['**/*.html'], + rules: { + 'import/unambiguous': 'off' + } + }, + // Our Markdown rules (and used for JSDoc examples as well, by way of + // our use of `matchingFileName` in conjunction with + // `jsdoc/check-examples` within `ash-nazg`) + { + files: ["**/*.md"], + rules: { + "eol-last": ["off"], + "no-console": ["off"], + "no-undef": ["off"], + "no-unused-vars": ["warn"], + "padded-blocks": ["off"], + "import/unambiguous": ["off"], + "import/no-unresolved": ["off"], + "node/no-missing-import": ["off"], + "no-multi-spaces": "off", + "sonarjs/no-all-duplicated-branches": "off", + "no-alert": "off", + // Disable until may fix https://github.com/gajus/eslint-plugin-jsdoc/issues/211 + "indent": "off" + } + }, + // Dis-apply Node rules mistakenly giving errors with browser files, + // and treating Node global `root` as being present for shadowing + { + files: ["editor/**", "test/**", "screencasts/**"], + globals: { + root: "off" + }, + rules: { + "node/no-unsupported-features/node-builtins": "off" + } + }, + // We want console in tests! + { + files: ["test/**"], + rules: { + "no-console": ["off"] + } + }, + { + // Node files + files: [ + "docs/jsdoc-config.js", + "build-html.js", + "rollup.config.js", "rollup-config.config.js" + ], + env: { + node: true, + }, + globals: { + require: true + }, + rules: { + // We can't put Rollup in npmignore or user can't get access, + // and we have too many modules to add to `peerDependencies` + // so this rule can know them to be available, so we instead + // disable + "node/no-unpublished-import": "off" + } + }, + { + // As consumed by jsdoc, cannot be expressed as ESM + files: ["docs/jsdoc-config.js"], + parserOptions: { + sourceType: "script" + }, + globals: { + "module": false + }, + rules: { + "import/no-commonjs": "off", + "strict": "off" + } + } + ], + rules: { + // The Babel transform seems to have a problem converting these + "prefer-named-capture-group": "off", + // Override these `ash-nazg/sauron` rules which are difficult for us + // to apply at this time + "unicorn/prefer-string-slice": "off", + "default-case": "off", + "require-unicode-regexp": "off", + "max-len": ["off", { + ignoreUrls: true, + ignoreRegExpLiterals: true + }], + "unicorn/prefer-query-selector": "off", + "unicorn/prefer-node-append": "off", + "unicorn/no-zero-fractions": "off" + } +}; diff --git a/src/image_occlusion_enhanced/svg-edit/.github/ISSUE_TEMPLATE/bug_report.md b/src/image_occlusion_enhanced/svg-edit/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..9005e2d3 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report in case we may be able to help + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**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. + +**SVG-Edit environment (IMPORTANT)** +- File for SVG-Edit: [e.g., `svg-editor.html`, `svg-editor-es.html`, `embedapi.html`] +- Version: [e.g. 3.0.0] +- Protocol: [e.g., http, https, file] + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/src/image_occlusion_enhanced/svg-edit/.gitignore b/src/image_occlusion_enhanced/svg-edit/.gitignore new file mode 100644 index 00000000..cc2cf75c --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/.gitignore @@ -0,0 +1,8 @@ +ignore +node_modules + +build/ + +svgedit-custom.css + +docs/jsdoc diff --git a/src/image_occlusion_enhanced/svg-edit/.htmlhintrc b/src/image_occlusion_enhanced/svg-edit/.htmlhintrc new file mode 100644 index 00000000..8412d07b --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/.htmlhintrc @@ -0,0 +1,3 @@ +{ + "title-require": false +} diff --git a/src/image_occlusion_enhanced/svg-edit/.npmignore b/src/image_occlusion_enhanced/svg-edit/.npmignore new file mode 100644 index 00000000..77a92f98 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/.npmignore @@ -0,0 +1,7 @@ +ignore +screencasts +test + +.github/ISSUE_TEMPLATE/bug_report.md +build +lgtm.yml diff --git a/src/image_occlusion_enhanced/svg-edit/.remarkrc b/src/image_occlusion_enhanced/svg-edit/.remarkrc new file mode 100644 index 00000000..da24a20e --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/.remarkrc @@ -0,0 +1,5 @@ +{ + "plugins": { + "lint-ordered-list-marker-value": "one" + } +} diff --git a/src/image_occlusion_enhanced/svg-edit/AUTHORS b/src/image_occlusion_enhanced/svg-edit/AUTHORS new file mode 100644 index 00000000..9ea98d20 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/AUTHORS @@ -0,0 +1,23 @@ +Narendra Sisodiya +Pavol Rusnak +Jeff Schiller +Vidar Hokstad +Alexis Deveria +Brett Zamir + +Translation credits: + +ar: Tarik Belaam (العربية) +cs: Jan Ptacek (Čeština) +de: Reimar Bauer (Deutsch) +es: Alicia Puerto (Español) +fa: Payman Delshad (فارسی) +fr: wormsxulla (Français) +fy: Wander Nauta (Frysk) +hi: Tavish Naruka (हिन्दी) +ja: Dong (日本語) +nl: Jaap Blom (Nederlands) +ro: Christian Tzurcanu (Româneşte) +ru: Laurent Dufloux (Русский) +sk: Pavol Rusnak (Slovenčina) +zh-TW: 黃瀚生 (Han Sheng Huang) (台灣正體) diff --git a/src/image_occlusion_enhanced/svg-edit/CHANGES.md b/src/image_occlusion_enhanced/svg-edit/CHANGES.md new file mode 100644 index 00000000..0d069f74 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/CHANGES.md @@ -0,0 +1,1159 @@ +# SVG-Edit CHANGES + +## 5.1.0 + +- Deprecated: Should now use `avoidClientSideDownload` in place of + `avoidClientSide` (config for `ext-server_opensave.js`). +- Fix: IE 11 issue; fixes #363 +- Fix: Avoid race condition in `jQuery.svgIcons.js` (evident + when attempting to load from `file:` URL in Chrome) +- Fix: Input width (@seahindeniz) +- Fix: Use change event to be sure that file is loaded before + the function (@Moliman) +- Enhancement: Added `avoidClientSideOpen` config for + `ext-server_opensave.js` +- Optimization: Re-rerun image optimization per update +- Linting (ESLint): Adjust per now applied rules +- Linting (ESLint): Add HTML files to linting +- Linting (ESLint): Avoid shadowing +- Linting: Assorted changes; ensure license versions are valid semver +- Refactoring: Use `static` keyword for classes +- Refactoring: Prefer `for...of`, `event.key` (newly enforced linting) +- Refactoring: Better var. names +- Testing: Accessibility test API update +- Docs: Clarify need for Node.js/npm being installed +- Build: Remove `types-docs` script as being handled in ESLint +- Maintenance: Add `.editorconfig` +- npm: Add script to fix eslint errors +- npm: Rename `build-config` to `build-by-config` +- npm: Update devDeps (including switching from deprecated opn-cli + package to open-cli); update core-js-bundle copy; add new + axe testing peer dep. axe-core in devDeps + +## 5.0.0 + +- Breaking change: Switch from deprecated `@babel/polyfill` to + `core-js-bundle` and `regenerator-runtime` replacements +- Build: Require Node 8.5 +- Fix: Ensure PHP files are present in `dist/extensions` alongside + JavaScript extension files using them +- Fix: Bug in obtaining `extPath` in `ext-server_opensave.js` +- Fix: Fully redirect extension entrances for lacking browser support +- Enhancement: Add config `avoidClientSide` to avoid using + client-side support by default (and always require server) +- Enhancement: Return a Promise for Editor's `setCustomHandlers`, + `loadFromString`, `loadFromDataURI` so known when ready and set +- Refactoring: Destructuring, templates, label Unicode code point +- Linting (JSDoc): Update per newly enforced `require-returns`; avoid + Closure syntax; reenable `jsdoc/valid-jsdoc` as fixed; notes + re: valid-jsdoc replacement; use same namepath +- Linting: Update per ash-nazg/plugin-node update +- Docs: Simplify comments in HTML files re: script purposes +- Docs: Update release instructions +- Docs (Refactoring): Formally specify `Promise` resolve type; + add `typedef` for dialog result object; add an + `ArbitraryCallbackResult` type; prefer `void` +- npm: Rename (`build-doc` to `build-docs`, `types-doc` to + `types-docs`); add `open-docs` script +- npm: Update devDeps +- npm: Remove unused devDeps; update insecure devDeps + +## 4.3.0 + +- Fix: Droplets for gradient pickers can now be double-clicked in + other browsers in addition to Firefox such as Chrome + to allow change of color (fixes #181) (@ajinkyas); + may be different between the browsers as a result of + +- Fix: Delay icon setting until locales available (fixes #323) +- Fix: Extension with no `placement` to be added to end; + for #326 (@sjernigan) +- Fix: Error on dragging control point of arc; fixes #268 (@cuixiping) +- Fix: With locales loading before extensions, ensure extensions' + `langReady` runs properly; fixes #334 (@cuixiping) +- Optimization fix: Properly run code conditionally on browser check; + fixes #312 (@ianli-sc) +- Enhancement: Add CAD Placemark extension (@NeiroNx) +- Enhancement (svgIcons): Fix JSDoc param def; add `alt` options +- Accessibility: Begin work, add aria-label to some buttons and + form controls; add `role=main`; ``; ` +``` + +Note that if you want support for the following browsers, you will at least +need some polyfills. + +For Android Browser 4.4.3-4.4.4, you will need at least `fetch`. + +For the following, you will need at least `URL`, `Promise`, and `fetch`: + +- IE <= 11 +- IE Mobile +- Opera Mini +- Blackberry Browser <= 10 + +And for still older browsers (e.g., IE 8), you will at minimum need a +`querySelector` polyfill. + +### Integrating SVG-edit into your own npm package + +These steps are only needed if you wish to set up your own npm package +incorporating SVGEdit. You will need to have Node.js/npm installed. + +1. Create and enter an empty folder somewhere on your desktop. +1. Create your npm package: `npm init` (complete the fields). +1. Install SVG-edit into your package: + `npm i --save svgedit`. +1. Look within `node_modules/svgedit/`, e.g., `node_modules/svgedit/editor/svg-editor.html` + for the files your package needs and use accordingly (from outside of + `node_modules`). +1. If you want to publish your own work, you can use `npm publish`. + +## Programmatic customization + +1. If you are not concerned about supporting ES6 Modules (see the + "ES6 Modules file" section), you can add your config directly to + `svgedit-config-iife.js` within the SVG-Edit project root. + 1. Note: Do not remove the `import svgEditor...` code which is responsible + for importing the SVG edit code. Versions prior to 3.0 did not require + this, but the advantage is that your HTML does not need to be polluted + with extra script references. +1. Modify or utilize any options. See `docs/tutorials/ConfigOptions.md` + ([ConfigOptions]{@tutorial ConfigOptions}). + +## ES6 Modules file + +1. `svg-editor-es.html` is an HTML file directly using ES6 modules. + It is only supported in the latest browsers. It is probably mostly + useful for debugging, as it requires more network requests. + If you would like to work with this file, you should make configuration + changes in `svgedit-config-es.js` (in the SVG-Edit project root). +1. If you are working with the ES6 Modules config but also wish to work with + the normal `svg-editor.html` version (so your code can work in older + browsers or get the presumable performance benefits of this file which + references JavaScript rolled up into a single file), you can follow these + steps after any config changes you make, so that your changes can also be + automatically made available to both versions. + 1. JavaScript: + 1. Run `npm install` within the svgedit directory + (`node_modules/svgedit` if you installed via npm) and the root + repository directory if you cloned the Git repository instead. + This will install the build tools for SVG-edit. + 1. Run `npm run build-by-config` within the svgedit directory mentioned + in the step above. + 1. This will rebuild `svgedit-config-iife.js` (applying Babel to + allow it to work on older browsers and applying Rollup to build + all JavaScript into one file). The file will then contain + non-ES6 module JavaScript that can work in older browsers. + Note that it bundles all of SVGEdit, so it is to be expected + that this file will be much larger in size than the original + ES6 config file. + 1. HTML: + 1. If you wish to make changes to both HTML files, it is recommended that + you work and test on `svg-editor-es.html` and then run + `npm run build-html` to have the changes properly copied to + `svg-editor.html`. + +## Recent news + +- 2019-11-16 Published 5.1.0 Misc. fixes and refactoring +- 2019-05-07 Published 5.0.0 Change from `@babel/polyfill` +- 2019-04-03 Published 4.3.0 Fix for double click on gradient + picker droplets affecting some browsers and dragging control + point of arc. Other misc. fixes. Some accessibility and i18n. +- 2018-12-13 Published 4.2.0 (Chinese (simplified) and Russian locale + updates; retaining lines with grid mode) +- 2018-11-29 Published 4.1.0 (Fix for hyphenated locales, svgcanvas + distributions) +- 2018-11-16 Published 4.0.0/4.0.1 (Move to Promise-based APIs) +- 2018-11-01 Published 3.2.0 (Update qunit to resolve security vulnerability + of a dependency) +- 2018-10-25 Published 3.1.1 (Fix for saving SVG on Firefox) +- 2018-10-24 Published 3.1.0 (Redirect on modular page for non-module-support; + versions document (for migrating)) +- 2018-10-22 Published 3.0.1 (Revert fix affecting polygon selection) +- 2018-10-21 Published 3.0.0 (misc. improvements including centering canvas and + key locale fixes since last RC) +- 2018-09-30 Published 3.0.0-rc.3 with security and other fixes +- 2018-07-31 Published 3.0.0-rc.2 with misc. fixes +- 2018-07-19 Published 3.0.0-rc.1 allowing for extensions and locales to be + expressed as modules +- 2018-05-26 Published 3.0.0-alpha.2 with ES6 Modules support +- 2017-07 Added to Packagist: https://packagist.org/packages/svg-edit/svgedit +- 2015-12-02 SVG-edit 2.8.1 was released. +- 2015-11-24 SVG-edit 2.8 was released. +- 2015-11-24 Code, issue tracking, and docs are being moved to github + (previously [code.google.com](https://code.google.com/p/svg-edit)). +- 2014-04-17 2.7 and stable branches updated to reflect 2.7.1 important bug + fixes for the embedded editor. +- 2014-04-07 SVG-edit 2.7 was released. +- 2013-01-15 SVG-edit 2.6 was released. + +## Videos + + * [SVG-edit 2.4 Part 1](https://www.youtube.com/watch?v=zpC7b1ZJvvM) + * [SVG-edit 2.4 Part 2](https://www.youtube.com/watch?v=mDzZEoGUDe8) + * [SVG-edit 2.3 Features](https://www.youtube.com/watch?v=RVIcIy5fXOc) + * [Introduction to SVG-edit](https://www.youtube.com/watch?v=ZJKmEI06YiY) (Version 2.2) + +## Supported browsers + +The following browsers had been tested for 2.6 or earlier and will +probably continue to work with 3.0. + +- Firefox 1.5+ +- Opera 9.50+ +- Safari 4+ +- Chrome 1+ +- IE 9+ and Edge + +## Further reading and more information + + * See [docs](docs/) for more documentation. See the + [JSDocs for our latest release](https://svg-edit.github.io/svgedit/releases/latest/docs/jsdoc/index.html). + * [Acknowledgements](docs/Acknowledgements.md) lists open source projects + used in svg-edit. + * See [AUTHORS](AUTHORS) file for authors. + * [StackOverflow](https://stackoverflow.com/tags/svg-edit) group. + * Join the [svg-edit mailing list](https://groups.google.com/forum/#!forum/svg-edit). + * Join us on `#svg-edit` on `freenode.net` (or use the + [web client](https://webchat.freenode.net/?channels=svg-edit)). diff --git a/src/image_occlusion_enhanced/svg-edit/build-html.js b/src/image_occlusion_enhanced/svg-edit/build-html.js new file mode 100644 index 00000000..c84c32da --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/build-html.js @@ -0,0 +1,148 @@ +/* eslint-env node */ +import fs from 'promise-fs'; + +const filesAndReplacements = [ + { + input: 'editor/svg-editor-es.html', + output: 'editor/xdomain-svg-editor-es.html', + replacements: [ + [ + '', + '' + ] + ] + }, + { + input: 'editor/xdomain-svg-editor-es.html', + output: 'editor/xdomain-svg-editor.html', + replacements: [ + [ + '', + ` +` + ], + [ + '', + '' + ], + [ + '', + '' + ], + [ + '', + '' + ], + [ + '', + '' + ] + ] + }, + // Now that file has copied, we can replace the DOCTYPE in xdomain + { + input: 'editor/xdomain-svg-editor-es.html', + output: 'editor/xdomain-svg-editor-es.html', + replacements: [ + [ + '', + ` +` + ] + ] + }, + { + input: 'editor/svg-editor-es.html', + output: 'editor/svg-editor.html', + replacements: [ + [ + '', + ` +` + ], + [ + '', + '' + ], + [ + '', + '' + ], + [ + '', + '' + ], + [ + '', + '' + ] + ] + }, + { + input: 'editor/extensions/imagelib/openclipart-es.html', + output: 'editor/extensions/imagelib/openclipart.html', + replacements: [ + [ + '', + ` +` + ], + [ + '', + '' + ], + [ + '', + '' + ], + [ + '', + '' + ] + ] + }, + { + input: 'editor/extensions/imagelib/index-es.html', + output: 'editor/extensions/imagelib/index.html', + replacements: [ + [ + '', + ` +` + ], + [ + '', + '' + ], + [ + '', + '' + ] + ] + } +]; + +(async () => { +await filesAndReplacements.reduce(async (p, {input, output, replacements}) => { + await p; + let data; + try { + data = await fs.readFile(input, 'utf8'); + } catch (err) { + console.log(`Error reading ${input} file`, err); // eslint-disable-line no-console + } + + data = replacements.reduce((s, [fnd, replacement]) => { + return s.replace(fnd, replacement); + }, data); + + try { + await fs.writeFile(output, data); + } catch (err) { + console.log(`Error writing file: ${err}`, err); // eslint-disable-line no-console + return; + } + console.log(`Completed file ${input} rewriting!`); // eslint-disable-line no-console +}, Promise.resolve()); +console.log('Finished!'); // eslint-disable-line no-console +})(); diff --git a/src/image_occlusion_enhanced/svg-edit/chrome-app/icon_128.png b/src/image_occlusion_enhanced/svg-edit/chrome-app/icon_128.png new file mode 100644 index 00000000..6369b49b Binary files /dev/null and b/src/image_occlusion_enhanced/svg-edit/chrome-app/icon_128.png differ diff --git a/src/image_occlusion_enhanced/svg-edit/chrome-app/manifest.json b/src/image_occlusion_enhanced/svg-edit/chrome-app/manifest.json new file mode 100644 index 00000000..80dba56d --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/chrome-app/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "SVG-edit", + "description": "A fast, web-based, Javascript-driven SVG editor that works in any modern browser", + "version": "2.6", + "app": { + "urls": [ + "*://svg-edit.googlecode.com/svn/tags/stable/" + ], + "launch": { + "web_url": "http://svg-edit.googlecode.com/svn/tags/stable/editor/svg-editor.html" + } + }, + "icons": { + "128": "icon_128.png" + }, + "permissions": [ + "unlimitedStorage", + "notifications" + ] +} diff --git a/src/image_occlusion_enhanced/svg-edit/clipart/moon.svg b/src/image_occlusion_enhanced/svg-edit/clipart/moon.svg new file mode 100644 index 00000000..179fa669 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/clipart/moon.svg @@ -0,0 +1,13 @@ + + + + Layer 1 + + + + + + + + + diff --git a/src/image_occlusion_enhanced/svg-edit/clipart/star.svg b/src/image_occlusion_enhanced/svg-edit/clipart/star.svg new file mode 100644 index 00000000..62bb89a4 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/clipart/star.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/image_occlusion_enhanced/svg-edit/clipart/sun.svg b/src/image_occlusion_enhanced/svg-edit/clipart/sun.svg new file mode 100644 index 00000000..df2f5b77 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/clipart/sun.svg @@ -0,0 +1,3 @@ + + +image/svg+xml diff --git a/src/image_occlusion_enhanced/svg-edit/composer.json b/src/image_occlusion_enhanced/svg-edit/composer.json new file mode 100644 index 00000000..a4da44ed --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/composer.json @@ -0,0 +1,42 @@ +{ + "name": "svg-edit/svgedit", + "description": "SVG-edit is a fast, web-based, javascript-driven SVG drawing editor that works in any modern browser.", + "authors": [ + { + "name": "Narendra Sisodiya", + "email": "narendra@narendrasisodiya.com" + }, + { + "name": "Pavol Rusnak", + "email": "stick@gk2.sk" + }, + { + "name": "Jeff Schiller", + "email": "codedread@gmail.com" + }, + { + "name": "Vidar Hokstad", + "email": "vidar.hokstad@gmail.com" + }, + { + "name": "Alexis Deveria", + "email": "adeveria@gmail.com" + }, + { + "name": "Brett Zamir", + "email": "brettz9@yahoo.com" + } + ], + "keywords": [ + "svg", + "svg-edit", + "drawing", + "editor" + ], + "type": "library", + "license": "MIT", + "minimum-stability": "stable", + "homepage": "https://github.com/SVG-Edit/svgedit/tree/master", + "require": { + } +} diff --git a/src/image_occlusion_enhanced/svg-edit/demos/canvas.html b/src/image_occlusion_enhanced/svg-edit/demos/canvas.html new file mode 100644 index 00000000..bb5e055a --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/demos/canvas.html @@ -0,0 +1,56 @@ + + + + + Minimal demo of SvgCanvas + + + + + + + +

      Minimal demo of SvgCanvas

      + +
      + +
      + [ + + ] + + + + +
      + + + + diff --git a/src/image_occlusion_enhanced/svg-edit/dist/canvg.js b/src/image_occlusion_enhanced/svg-edit/dist/canvg.js new file mode 100644 index 00000000..38992de2 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/canvg.js @@ -0,0 +1,4839 @@ +var canvg = (function (exports) { + 'use strict'; + + function _typeof(obj) { + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function (obj) { + return typeof obj; + }; + } else { + _typeof = function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); + } + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function"); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + writable: true, + configurable: true + } + }); + if (superClass) _setPrototypeOf(subClass, superClass); + } + + function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); + } + + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + + return _setPrototypeOf(o, p); + } + + function isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + + try { + Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); + return true; + } catch (e) { + return false; + } + } + + function _construct(Parent, args, Class) { + if (isNativeReflectConstruct()) { + _construct = Reflect.construct; + } else { + _construct = function _construct(Parent, args, Class) { + var a = [null]; + a.push.apply(a, args); + var Constructor = Function.bind.apply(Parent, a); + var instance = new Constructor(); + if (Class) _setPrototypeOf(instance, Class.prototype); + return instance; + }; + } + + return _construct.apply(null, arguments); + } + + function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return self; + } + + function _possibleConstructorReturn(self, call) { + if (call && (typeof call === "object" || typeof call === "function")) { + return call; + } + + return _assertThisInitialized(self); + } + + function _superPropBase(object, property) { + while (!Object.prototype.hasOwnProperty.call(object, property)) { + object = _getPrototypeOf(object); + if (object === null) break; + } + + return object; + } + + function _get(target, property, receiver) { + if (typeof Reflect !== "undefined" && Reflect.get) { + _get = Reflect.get; + } else { + _get = function _get(target, property, receiver) { + var base = _superPropBase(target, property); + + if (!base) return; + var desc = Object.getOwnPropertyDescriptor(base, property); + + if (desc.get) { + return desc.get.call(receiver); + } + + return desc.value; + }; + } + + return _get(target, property, receiver || target); + } + + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); + } + + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); + } + + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; + + return arr2; + } + } + + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; + } + + function _iterableToArray(iter) { + if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); + } + + function _iterableToArrayLimit(arr, i) { + if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { + return; + } + + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance"); + } + + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + + /** + * For parsing color values + * @module RGBColor + * @author Stoyan Stefanov + * @see https://www.phpied.com/rgb-color-parser-in-javascript/ + * @license MIT + */ + var simpleColors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred: 'cd5c5c', + indigo: '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' + }; // array of color definition objects + + var colorDefs = [{ + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + // re: /^rgb\((?\d{1,3}),\s*(?\d{1,3}),\s*(?\d{1,3})\)$/, + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], + process: function process(_) { + for (var _len = arguments.length, bits = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + bits[_key - 1] = arguments[_key]; + } + + return bits.map(function (b) { + return parseInt(b); + }); + } + }, { + re: /^(\w{2})(\w{2})(\w{2})$/, + // re: /^(?\w{2})(?\w{2})(?\w{2})$/, + example: ['#00ff00', '336699'], + process: function process(_) { + for (var _len2 = arguments.length, bits = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + bits[_key2 - 1] = arguments[_key2]; + } + + return bits.map(function (b) { + return parseInt(b, 16); + }); + } + }, { + re: /^(\w{1})(\w{1})(\w{1})$/, + // re: /^(?\w{1})(?\w{1})(?\w{1})$/, + example: ['#fb0', 'f0f'], + process: function process(_) { + for (var _len3 = arguments.length, bits = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { + bits[_key3 - 1] = arguments[_key3]; + } + + return bits.map(function (b) { + return parseInt(b + b, 16); + }); + } + }]; + /** + * A class to parse color values. + */ + + var RGBColor = + /*#__PURE__*/ + function () { + /** + * @param {string} colorString + */ + function RGBColor(colorString) { + var _this = this; + + _classCallCheck(this, RGBColor); + + this.ok = false; // strip any leading # + + if (colorString.charAt(0) === '#') { + // remove # if any + colorString = colorString.substr(1, 6); + } + + colorString = colorString.replace(/ /g, ''); + colorString = colorString.toLowerCase(); // before getting into regexps, try simple matches + // and overwrite the input + + if (colorString in simpleColors) { + colorString = simpleColors[colorString]; + } // end of simple type-in colors + // search through the definitions to find a match + + + colorDefs.forEach(function (_ref) { + var re = _ref.re, + processor = _ref.process; + var bits = re.exec(colorString); + + if (bits) { + var _processor = processor.apply(void 0, _toConsumableArray(bits)), + _processor2 = _slicedToArray(_processor, 3), + r = _processor2[0], + g = _processor2[1], + b = _processor2[2]; + + Object.assign(_this, { + r: r, + g: g, + b: b + }); + _this.ok = true; + } + }); // validate/cleanup values + + this.r = this.r < 0 || isNaN(this.r) ? 0 : this.r > 255 ? 255 : this.r; + this.g = this.g < 0 || isNaN(this.g) ? 0 : this.g > 255 ? 255 : this.g; + this.b = this.b < 0 || isNaN(this.b) ? 0 : this.b > 255 ? 255 : this.b; + } // some getters + + /** + * @returns {string} + */ + + + _createClass(RGBColor, [{ + key: "toRGB", + value: function toRGB() { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + } + /** + * @returns {string} + */ + + }, { + key: "toHex", + value: function toHex() { + var r = this.r.toString(16); + var g = this.g.toString(16); + var b = this.b.toString(16); + + if (r.length === 1) { + r = '0' + r; + } + + if (g.length === 1) { + g = '0' + g; + } + + if (b.length === 1) { + b = '0' + b; + } + + return '#' + r + g + b; + } + /** + * Offers a bulleted list of help. + * @returns {HTMLUListElement} + */ + + }], [{ + key: "getHelpXML", + value: function getHelpXML() { + var examples = [].concat(_toConsumableArray(colorDefs.flatMap(function (_ref2) { + var example = _ref2.example; + return example; + })), _toConsumableArray(Object.keys(simpleColors))); + var xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + xml.append.apply(xml, _toConsumableArray(examples.map(function (example) { + try { + var listItem = document.createElement('li'); + var listColor = new RGBColor(example); + var exampleDiv = document.createElement('div'); + exampleDiv.style.cssText = "\n margin: 3px;\n border: 1px solid black;\n background: ".concat(listColor.toHex(), ";\n color: ").concat(listColor.toHex(), ";"); + exampleDiv.append('test'); + var listItemValue = " ".concat(example, " -> ").concat(listColor.toRGB(), " -> ").concat(listColor.toHex()); + listItem.append(exampleDiv, listItemValue); + return listItem; + } catch (e) { + return ''; + } + }))); + return xml; + } + }]); + + return RGBColor; + }(); + + function _typeof$1(obj) { + if (typeof Symbol === "function" && _typeof(Symbol.iterator) === "symbol") { + _typeof$1 = function _typeof$1(obj) { + return _typeof(obj); + }; + } else { + _typeof$1 = function _typeof$1(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof(obj); + }; + } + + return _typeof$1(obj); + } + + function _classCallCheck$1(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + /** + * StackBlur - a fast almost Gaussian Blur For Canvas + * + * In case you find this class useful - especially in commercial projects - + * I am not totally unhappy for a small donation to my PayPal account + * mario@quasimondo.de + * + * Or support me on flattr: + * {@link https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript} + * @module StackBlur + * @version 0.5 + * @author Mario Klingemann + * Contact: mario@quasimondo.com + * Website: {@link http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html} + * Twitter: @quasimondo + * + * @copyright (c) 2010 Mario Klingemann + * + * 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. + */ + + + var mulTable = [512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259]; + var shgTable = [9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24]; + /** + * @param {string|HTMLCanvasElement} canvas + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @throws {Error|TypeError} + * @returns {ImageData} See {@link https://html.spec.whatwg.org/multipage/canvas.html#imagedata} + */ + + + function getImageDataFromCanvas(canvas, topX, topY, width, height) { + if (typeof canvas === 'string') { + canvas = document.getElementById(canvas); + } + + if (!canvas || _typeof$1(canvas) !== 'object' || !('getContext' in canvas)) { + throw new TypeError('Expecting canvas with `getContext` method in processCanvasRGB(A) calls!'); + } + + var context = canvas.getContext('2d'); + + try { + return context.getImageData(topX, topY, width, height); + } catch (e) { + throw new Error('unable to access image data: ' + e); + } + } + /** + * @param {HTMLCanvasElement} canvas + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {undefined} + */ + + + function processCanvasRGBA(canvas, topX, topY, width, height, radius) { + if (isNaN(radius) || radius < 1) { + return; + } + + radius |= 0; + var imageData = getImageDataFromCanvas(canvas, topX, topY, width, height); + imageData = processImageDataRGBA(imageData, topX, topY, width, height, radius); + canvas.getContext('2d').putImageData(imageData, topX, topY); + } + /** + * @param {ImageData} imageData + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {ImageData} + */ + + + function processImageDataRGBA(imageData, topX, topY, width, height, radius) { + var pixels = imageData.data; + var x, y, i, p, yp, yi, yw, rSum, gSum, bSum, aSum, rOutSum, gOutSum, bOutSum, aOutSum, rInSum, gInSum, bInSum, aInSum, pr, pg, pb, pa, rbs; + var div = 2 * radius + 1; // const w4 = width << 2; + + var widthMinus1 = width - 1; + var heightMinus1 = height - 1; + var radiusPlus1 = radius + 1; + var sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2; + var stackStart = new BlurStack(); + var stack = stackStart; + var stackEnd; + + for (i = 1; i < div; i++) { + stack = stack.next = new BlurStack(); + + if (i === radiusPlus1) { + stackEnd = stack; + } + } + + stack.next = stackStart; + var stackIn = null; + var stackOut = null; + yw = yi = 0; + var mulSum = mulTable[radius]; + var shgSum = shgTable[radius]; + + for (y = 0; y < height; y++) { + rInSum = gInSum = bInSum = aInSum = rSum = gSum = bSum = aSum = 0; + rOutSum = radiusPlus1 * (pr = pixels[yi]); + gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); + bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); + aOutSum = radiusPlus1 * (pa = pixels[yi + 3]); + rSum += sumFactor * pr; + gSum += sumFactor * pg; + bSum += sumFactor * pb; + aSum += sumFactor * pa; + stack = stackStart; + + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + + for (i = 1; i < radiusPlus1; i++) { + p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); + rSum += (stack.r = pr = pixels[p]) * (rbs = radiusPlus1 - i); + gSum += (stack.g = pg = pixels[p + 1]) * rbs; + bSum += (stack.b = pb = pixels[p + 2]) * rbs; + aSum += (stack.a = pa = pixels[p + 3]) * rbs; + rInSum += pr; + gInSum += pg; + bInSum += pb; + aInSum += pa; + stack = stack.next; + } + + stackIn = stackStart; + stackOut = stackEnd; + + for (x = 0; x < width; x++) { + pixels[yi + 3] = pa = aSum * mulSum >> shgSum; + + if (pa !== 0) { + pa = 255 / pa; + pixels[yi] = (rSum * mulSum >> shgSum) * pa; + pixels[yi + 1] = (gSum * mulSum >> shgSum) * pa; + pixels[yi + 2] = (bSum * mulSum >> shgSum) * pa; + } else { + pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0; + } + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + aSum -= aOutSum; + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + aOutSum -= stackIn.a; + p = yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1) << 2; + rInSum += stackIn.r = pixels[p]; + gInSum += stackIn.g = pixels[p + 1]; + bInSum += stackIn.b = pixels[p + 2]; + aInSum += stackIn.a = pixels[p + 3]; + rSum += rInSum; + gSum += gInSum; + bSum += bInSum; + aSum += aInSum; + stackIn = stackIn.next; + rOutSum += pr = stackOut.r; + gOutSum += pg = stackOut.g; + bOutSum += pb = stackOut.b; + aOutSum += pa = stackOut.a; + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + aInSum -= pa; + stackOut = stackOut.next; + yi += 4; + } + + yw += width; + } + + for (x = 0; x < width; x++) { + gInSum = bInSum = aInSum = rInSum = gSum = bSum = aSum = rSum = 0; + yi = x << 2; + rOutSum = radiusPlus1 * (pr = pixels[yi]); + gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); + bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); + aOutSum = radiusPlus1 * (pa = pixels[yi + 3]); + rSum += sumFactor * pr; + gSum += sumFactor * pg; + bSum += sumFactor * pb; + aSum += sumFactor * pa; + stack = stackStart; + + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + + yp = width; + + for (i = 1; i <= radius; i++) { + yi = yp + x << 2; + rSum += (stack.r = pr = pixels[yi]) * (rbs = radiusPlus1 - i); + gSum += (stack.g = pg = pixels[yi + 1]) * rbs; + bSum += (stack.b = pb = pixels[yi + 2]) * rbs; + aSum += (stack.a = pa = pixels[yi + 3]) * rbs; + rInSum += pr; + gInSum += pg; + bInSum += pb; + aInSum += pa; + stack = stack.next; + + if (i < heightMinus1) { + yp += width; + } + } + + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + + for (y = 0; y < height; y++) { + p = yi << 2; + pixels[p + 3] = pa = aSum * mulSum >> shgSum; + + if (pa > 0) { + pa = 255 / pa; + pixels[p] = (rSum * mulSum >> shgSum) * pa; + pixels[p + 1] = (gSum * mulSum >> shgSum) * pa; + pixels[p + 2] = (bSum * mulSum >> shgSum) * pa; + } else { + pixels[p] = pixels[p + 1] = pixels[p + 2] = 0; + } + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + aSum -= aOutSum; + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + aOutSum -= stackIn.a; + p = x + ((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width << 2; + rSum += rInSum += stackIn.r = pixels[p]; + gSum += gInSum += stackIn.g = pixels[p + 1]; + bSum += bInSum += stackIn.b = pixels[p + 2]; + aSum += aInSum += stackIn.a = pixels[p + 3]; + stackIn = stackIn.next; + rOutSum += pr = stackOut.r; + gOutSum += pg = stackOut.g; + bOutSum += pb = stackOut.b; + aOutSum += pa = stackOut.a; + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + aInSum -= pa; + stackOut = stackOut.next; + yi += width; + } + } + + return imageData; + } + /** + * + */ + + + var BlurStack = function BlurStack() { + _classCallCheck$1(this, BlurStack); + + this.r = 0; + this.g = 0; + this.b = 0; + this.a = 0; + this.next = null; + }; + + /** + * Whether a value is `null` or `undefined`. + * @param {any} val + * @returns {boolean} + */ + + var isNullish = function isNullish(val) { + return val === null || val === undefined; + }; + /** + * @callback module:canvg.ForceRedraw + * @returns {boolean} + */ + + /** + * @typedef {PlainObject} module:canvg.CanvgOptions + * @property {boolean} opts.ignoreMouse true => ignore mouse events + * @property {boolean} opts.ignoreAnimation true => ignore animations + * @property {boolean} opts.ignoreDimensions true => does not try to resize canvas + * @property {boolean} opts.ignoreClear true => does not clear canvas + * @property {Integer} opts.offsetX int => draws at a x offset + * @property {Integer} opts.offsetY int => draws at a y offset + * @property {Integer} opts.scaleWidth int => scales horizontally to width + * @property {Integer} opts.scaleHeight int => scales vertically to height + * @property {module:canvg.ForceRedraw} opts.forceRedraw function => will call the function on every frame, if it returns true, will redraw + * @property {boolean} opts.log Adds log function + * @property {boolean} opts.useCORS Whether to set CORS `crossOrigin` for the image to `Anonymous` + */ + + /** + * If called with no arguments, it will replace all `` elements on the page + * with `` elements. + * @function module:canvg.canvg + * @param {HTMLCanvasElement|string} target canvas element or the id of a canvas element + * @param {string|XMLDocument} s - svg string, url to svg file, or xml document + * @param {module:canvg.CanvgOptions} [opts] Optional hash of options + * @returns {Promise} All the function after the first render is completed with dom + */ + + + var canvg = function canvg(target, s, opts) { + // no parameters + if (isNullish(target) && isNullish(s) && isNullish(opts)) { + var svgTags = document.querySelectorAll('svg'); + return Promise.all(_toConsumableArray(svgTags).map(function (svgTag) { + var c = document.createElement('canvas'); + c.width = svgTag.clientWidth; + c.height = svgTag.clientHeight; + svgTag.before(c); + svgTag.remove(); + var div = document.createElement('div'); + div.append(svgTag); + return canvg(c, div.innerHTML); + })); + } + + if (typeof target === 'string') { + target = document.getElementById(target); + } // store class on canvas + + + if (!isNullish(target.svg)) target.svg.stop(); + var svg = build(opts || {}); // on i.e. 8 for flash canvas, we can't assign the property so check for it + + if (!(target.childNodes.length === 1 && target.childNodes[0].nodeName === 'OBJECT')) { + target.svg = svg; + } + + var ctx = target.getContext('2d'); + + if (typeof s.documentElement !== 'undefined') { + // load from xml doc + return svg.loadXmlDoc(ctx, s); + } + + if (s.substr(0, 1) === '<') { + // load from xml string + return svg.loadXml(ctx, s); + } // load from url + + + return svg.load(ctx, s); + }; + /* eslint-disable jsdoc/check-types */ + + /** + * @param {module:canvg.CanvgOptions} opts + * @returns {object} + * @todo Flesh out exactly what object is returned here (after updating to latest and reincluding our changes here and those of StackBlur) + */ + + function build(opts) { + /* eslint-enable jsdoc/check-types */ + var svg = { + opts: opts + }; + svg.FRAMERATE = 30; + svg.MAX_VIRTUAL_PIXELS = 30000; + + svg.log = function (msg) { + /* */ + }; + + if (svg.opts.log === true && typeof console !== 'undefined') { + svg.log = function (msg) { + console.log(msg); + }; // eslint-disable-line no-console + + } // globals + + + svg.init = function (ctx) { + var uniqueId = 0; + + svg.UniqueId = function () { + uniqueId++; + return 'canvg' + uniqueId; + }; + + svg.Definitions = {}; + svg.Styles = {}; + svg.Animations = []; + svg.Images = []; + svg.ctx = ctx; + svg.ViewPort = { + viewPorts: [], + Clear: function Clear() { + this.viewPorts = []; + }, + SetCurrent: function SetCurrent(width, height) { + this.viewPorts.push({ + width: width, + height: height + }); + }, + RemoveCurrent: function RemoveCurrent() { + this.viewPorts.pop(); + }, + Current: function Current() { + return this.viewPorts[this.viewPorts.length - 1]; + }, + width: function width() { + return this.Current().width; + }, + height: function height() { + return this.Current().height; + }, + ComputeSize: function ComputeSize(d) { + if (!isNullish(d) && typeof d === 'number') return d; + if (d === 'x') return this.width(); + if (d === 'y') return this.height(); + return Math.sqrt(Math.pow(this.width(), 2) + Math.pow(this.height(), 2)) / Math.sqrt(2); + } + }; + }; + + svg.init(); // images loaded + + svg.ImagesLoaded = function () { + return svg.Images.every(function (img) { + return img.loaded; + }); + }; // trim + + + svg.trim = function (s) { + return s.replace(/^\s+|\s+$/g, ''); + }; // compress spaces + + + svg.compressSpaces = function (s) { + return s.replace(/[\s\r\t\n]+/gm, ' '); + }; // ajax + // Todo: Replace with `fetch` and polyfill + + + svg.ajax = function (url, asynch) { + var AJAX = window.XMLHttpRequest ? new XMLHttpRequest() : new window.ActiveXObject('Microsoft.XMLHTTP'); + + if (asynch) { + return new Promise(function (resolve, reject) { + // eslint-disable-line promise/avoid-new + var req = AJAX.open('GET', url, true); + req.addEventListener('load', function () { + resolve(AJAX.responseText); + }); + AJAX.send(null); + }); + } + + AJAX.open('GET', url, false); + AJAX.send(null); + return AJAX.responseText; + }; // parse xml + + + svg.parseXml = function (xml) { + if (window.DOMParser) { + var parser = new DOMParser(); + return parser.parseFromString(xml, 'text/xml'); + } + + xml = xml.replace(/]*>/, ''); + var xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + }; // text extensions + // get the text baseline + + + var textBaselineMapping = { + baseline: 'alphabetic', + 'before-edge': 'top', + 'text-before-edge': 'top', + middle: 'middle', + central: 'middle', + 'after-edge': 'bottom', + 'text-after-edge': 'bottom', + ideographic: 'ideographic', + alphabetic: 'alphabetic', + hanging: 'hanging', + mathematical: 'alphabetic' + }; + + svg.Property = + /*#__PURE__*/ + function () { + function Property(name, value) { + _classCallCheck(this, Property); + + this.name = name; + this.value = value; + } + + _createClass(Property, [{ + key: "getValue", + value: function getValue() { + return this.value; + } + }, { + key: "hasValue", + value: function hasValue() { + return !isNullish(this.value) && this.value !== ''; + } // return the numerical value of the property + + }, { + key: "numValue", + value: function numValue() { + if (!this.hasValue()) return 0; + var n = parseFloat(this.value); + + if (String(this.value).endsWith('%')) { + n /= 100.0; + } + + return n; + } + }, { + key: "valueOrDefault", + value: function valueOrDefault(def) { + if (this.hasValue()) return this.value; + return def; + } + }, { + key: "numValueOrDefault", + value: function numValueOrDefault(def) { + if (this.hasValue()) return this.numValue(); + return def; + } // color extensions + // augment the current color value with the opacity + + }, { + key: "addOpacity", + value: function addOpacity(opacityProp) { + var newValue = this.value; + + if (!isNullish(opacityProp.value) && opacityProp.value !== '' && typeof this.value === 'string') { + // can only add opacity to colors, not patterns + var color = new RGBColor(this.value); + + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')'; + } + } + + return new svg.Property(this.name, newValue); + } // definition extensions + // get the definition from the definitions table + + }, { + key: "getDefinition", + value: function getDefinition() { + var name = this.value.match(/#([^)'"]+)/); + + if (name) { + name = name[1]; + } + + if (!name) { + name = this.value; + } + + return svg.Definitions[name]; + } + }, { + key: "isUrlDefinition", + value: function isUrlDefinition() { + return this.value.startsWith('url('); + } + }, { + key: "getFillStyleDefinition", + value: function getFillStyleDefinition(e, opacityProp) { + var def = this.getDefinition(); // gradient + + if (!isNullish(def) && def.createGradient) { + return def.createGradient(svg.ctx, e, opacityProp); + } // pattern + + + if (!isNullish(def) && def.createPattern) { + if (def.getHrefAttribute().hasValue()) { + var pt = def.attribute('patternTransform'); + def = def.getHrefAttribute().getDefinition(); + + if (pt.hasValue()) { + def.attribute('patternTransform', true).value = pt.value; + } + } + + return def.createPattern(svg.ctx, e); + } + + return null; + } // length extensions + + }, { + key: "getDPI", + value: function getDPI(viewPort) { + return 96.0; // TODO: compute? + } + }, { + key: "getEM", + value: function getEM(viewPort) { + var em = 12; + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + if (fontSize.hasValue()) em = fontSize.toPixels(viewPort); + return em; + } + }, { + key: "getUnits", + value: function getUnits() { + return String(this.value).replace(/[0-9.-]/g, ''); + } // get the length as pixels + + }, { + key: "toPixels", + value: function toPixels(viewPort, processPercent) { + if (!this.hasValue()) return 0; + var s = String(this.value); + if (s.endsWith('em')) return this.numValue() * this.getEM(viewPort); + if (s.endsWith('ex')) return this.numValue() * this.getEM(viewPort) / 2.0; + if (s.endsWith('px')) return this.numValue(); + if (s.endsWith('pt')) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); + if (s.endsWith('pc')) return this.numValue() * 15; + if (s.endsWith('cm')) return this.numValue() * this.getDPI(viewPort) / 2.54; + if (s.endsWith('mm')) return this.numValue() * this.getDPI(viewPort) / 25.4; + if (s.endsWith('in')) return this.numValue() * this.getDPI(viewPort); + if (s.endsWith('%')) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); + var n = this.numValue(); + if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); + return n; + } // time extensions + // get the time as milliseconds + + }, { + key: "toMilliseconds", + value: function toMilliseconds() { + if (!this.hasValue()) return 0; + var s = String(this.value); + if (s.endsWith('ms')) return this.numValue(); + if (s.endsWith('s')) return this.numValue() * 1000; + return this.numValue(); + } // angle extensions + // get the angle as radians + + }, { + key: "toRadians", + value: function toRadians() { + if (!this.hasValue()) return 0; + var s = String(this.value); + if (s.endsWith('deg')) return this.numValue() * (Math.PI / 180.0); + if (s.endsWith('grad')) return this.numValue() * (Math.PI / 200.0); + if (s.endsWith('rad')) return this.numValue(); + return this.numValue() * (Math.PI / 180.0); + } + }, { + key: "toTextBaseline", + value: function toTextBaseline() { + if (!this.hasValue()) return null; + return textBaselineMapping[this.value]; + } + }]); + + return Property; + }(); // fonts + + + svg.Font = { + Styles: 'normal|italic|oblique|inherit', + Variants: 'normal|small-caps|inherit', + Weights: 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit', + CreateFont: function CreateFont(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { + var f = !isNullish(inherit) ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + return { + fontFamily: fontFamily || f.fontFamily, + fontSize: fontSize || f.fontSize, + fontStyle: fontStyle || f.fontStyle, + fontWeight: fontWeight || f.fontWeight, + fontVariant: fontVariant || f.fontVariant, + toString: function toString() { + return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' '); + } + }; + }, + Parse: function Parse(s) { + var _this = this; + + var f = {}; + var ds = svg.trim(svg.compressSpaces(s || '')).split(' '); + var set = { + fontSize: false, + fontStyle: false, + fontWeight: false, + fontVariant: false + }; + var ff = ''; + ds.forEach(function (d) { + if (!set.fontStyle && _this.Styles.includes(d)) { + if (d !== 'inherit') { + f.fontStyle = d; + } + + set.fontStyle = true; + } else if (!set.fontVariant && _this.Variants.includes(d)) { + if (d !== 'inherit') { + f.fontVariant = d; + } + + set.fontStyle = set.fontVariant = true; + } else if (!set.fontWeight && _this.Weights.includes(d)) { + if (d !== 'inherit') { + f.fontWeight = d; + } + + set.fontStyle = set.fontVariant = set.fontWeight = true; + } else if (!set.fontSize) { + if (d !== 'inherit') { + f.fontSize = d.split('/')[0]; + } + + set.fontStyle = set.fontVariant = set.fontWeight = set.fontSize = true; + } else if (d !== 'inherit') { + ff += d; + } + }); + + if (ff !== '') { + f.fontFamily = ff; + } + + return f; + } + }; // points and paths + + svg.ToNumberArray = function (s) { + var a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' '); + return a.map(function (_a) { + return parseFloat(_a); + }); + }; + + svg.Point = + /*#__PURE__*/ + function () { + function _class(x, y) { + _classCallCheck(this, _class); + + this.x = x; + this.y = y; + } + + _createClass(_class, [{ + key: "angleTo", + value: function angleTo(p) { + return Math.atan2(p.y - this.y, p.x - this.x); + } + }, { + key: "applyTransform", + value: function applyTransform(v) { + var xp = this.x * v[0] + this.y * v[2] + v[4]; + var yp = this.x * v[1] + this.y * v[3] + v[5]; + this.x = xp; + this.y = yp; + } + }]); + + return _class; + }(); + + svg.CreatePoint = function (s) { + var a = svg.ToNumberArray(s); + return new svg.Point(a[0], a[1]); + }; + + svg.CreatePath = function (s) { + var a = svg.ToNumberArray(s); + var path = []; + + for (var i = 0; i < a.length; i += 2) { + path.push(new svg.Point(a[i], a[i + 1])); + } + + return path; + }; // bounding box + + + svg.BoundingBox = + /*#__PURE__*/ + function () { + function _class2(x1, y1, x2, y2) { + _classCallCheck(this, _class2); + + // pass in initial points if you want + this.x1 = Number.NaN; + this.y1 = Number.NaN; + this.x2 = Number.NaN; + this.y2 = Number.NaN; + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + + _createClass(_class2, [{ + key: "x", + value: function x() { + return this.x1; + } + }, { + key: "y", + value: function y() { + return this.y1; + } + }, { + key: "width", + value: function width() { + return this.x2 - this.x1; + } + }, { + key: "height", + value: function height() { + return this.y2 - this.y1; + } + }, { + key: "addPoint", + value: function addPoint(x, y) { + if (!isNullish(x)) { + if (isNaN(this.x1) || isNaN(this.x2)) { + this.x1 = x; + this.x2 = x; + } + + if (x < this.x1) this.x1 = x; + if (x > this.x2) this.x2 = x; + } + + if (!isNullish(y)) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + } + }, { + key: "addX", + value: function addX(x) { + this.addPoint(x, null); + } + }, { + key: "addY", + value: function addY(y) { + this.addPoint(null, y); + } + }, { + key: "addBoundingBox", + value: function addBoundingBox(bb) { + this.addPoint(bb.x1, bb.y1); + this.addPoint(bb.x2, bb.y2); + } + }, { + key: "addQuadraticCurve", + value: function addQuadraticCurve(p0x, p0y, p1x, p1y, p2x, p2y) { + var cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + + var cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + + var cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + + var cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + }, { + key: "addBezierCurve", + value: function addBezierCurve(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + var _this2 = this; + + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + var p0 = [p0x, p0y], + p1 = [p1x, p1y], + p2 = [p2x, p2y], + p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + var _loop = function _loop(i) { + var f = function f(t) { + return Math.pow(1 - t, 3) * p0[i] + 3 * Math.pow(1 - t, 2) * t * p1[i] + 3 * (1 - t) * Math.pow(t, 2) * p2[i] + Math.pow(t, 3) * p3[i]; + }; + + var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + var c = 3 * p1[i] - 3 * p0[i]; + + if (a === 0) { + if (b === 0) return "continue"; + var t = -c / b; + + if (t > 0 && t < 1) { + if (i === 0) _this2.addX(f(t)); + if (i === 1) _this2.addY(f(t)); + } + + return "continue"; + } + + var b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) return "continue"; + var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + + if (t1 > 0 && t1 < 1) { + if (i === 0) _this2.addX(f(t1)); + if (i === 1) _this2.addY(f(t1)); + } + + var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + + if (t2 > 0 && t2 < 1) { + if (i === 0) _this2.addX(f(t2)); + if (i === 1) _this2.addY(f(t2)); + } + }; + + for (var i = 0; i <= 1; i++) { + var _ret = _loop(i); + + if (_ret === "continue") continue; + } + } + }, { + key: "isPointInBox", + value: function isPointInBox(x, y) { + return this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2; + } + }]); + + return _class2; + }(); // transforms + + + svg.Transform = + /*#__PURE__*/ + function () { + function _class3(v) { + var _this6 = this; + + _classCallCheck(this, _class3); + + this.Type = { + translate: function translate(s) { + _classCallCheck(this, translate); + + this.p = svg.CreatePoint(s); + + this.apply = function (ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + }; + + this.unapply = function (ctx) { + ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0); + }; + + this.applyToPoint = function (p) { + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + }; + }, + rotate: function rotate(s) { + _classCallCheck(this, rotate); + + var a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + + this.apply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + + this.unapply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(-1.0 * this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + + this.applyToPoint = function (p) { + var _a = this.angle.toRadians(); + + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + p.applyTransform([Math.cos(_a), Math.sin(_a), -Math.sin(_a), Math.cos(_a), 0, 0]); + p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); + }; + }, + scale: function scale(s) { + _classCallCheck(this, scale); + + this.p = svg.CreatePoint(s); + + this.apply = function (ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + }; + + this.unapply = function (ctx) { + ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0); + }; + + this.applyToPoint = function (p) { + p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); + }; + }, + matrix: function matrix(s) { + _classCallCheck(this, matrix); + + this.m = svg.ToNumberArray(s); + + this.apply = function (ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + }; + + this.applyToPoint = function (p) { + p.applyTransform(this.m); + }; + } + }; + Object.assign(this.Type, { + SkewBase: + /*#__PURE__*/ + function (_this$Type$matrix) { + _inherits(SkewBase, _this$Type$matrix); + + function SkewBase(s) { + var _this3; + + _classCallCheck(this, SkewBase); + + _this3 = _possibleConstructorReturn(this, _getPrototypeOf(SkewBase).call(this, s)); + _this3.angle = new svg.Property('angle', s); + return _this3; + } + + return SkewBase; + }(this.Type.matrix) + }); + Object.assign(this.Type, { + skewX: + /*#__PURE__*/ + function (_this$Type$SkewBase) { + _inherits(skewX, _this$Type$SkewBase); + + function skewX(s) { + var _this4; + + _classCallCheck(this, skewX); + + _this4 = _possibleConstructorReturn(this, _getPrototypeOf(skewX).call(this, s)); + _this4.m = [1, 0, Math.tan(_this4.angle.toRadians()), 1, 0, 0]; + return _this4; + } + + return skewX; + }(this.Type.SkewBase), + skewY: + /*#__PURE__*/ + function (_this$Type$SkewBase2) { + _inherits(skewY, _this$Type$SkewBase2); + + function skewY(s) { + var _this5; + + _classCallCheck(this, skewY); + + _this5 = _possibleConstructorReturn(this, _getPrototypeOf(skewY).call(this, s)); + _this5.m = [1, Math.tan(_this5.angle.toRadians()), 0, 1, 0, 0]; + return _this5; + } + + return skewY; + }(this.Type.SkewBase) + }); + var data = svg.trim(svg.compressSpaces(v)).replace(/\)([a-zA-Z])/g, ') $1').replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/); + this.transforms = data.map(function (d) { + var type = svg.trim(d.split('(')[0]); + var s = d.split('(')[1].replace(')', ''); + var transform = new _this6.Type[type](s); + transform.type = type; + return transform; + }); + } + + _createClass(_class3, [{ + key: "apply", + value: function apply(ctx) { + this.transforms.forEach(function (transform) { + transform.apply(ctx); + }); + } + }, { + key: "unapply", + value: function unapply(ctx) { + for (var i = this.transforms.length - 1; i >= 0; i--) { + this.transforms[i].unapply(ctx); + } + } + }, { + key: "applyToPoint", + value: function applyToPoint(p) { + this.transforms.forEach(function (transform) { + transform.applyToPoint(p); + }); + } + }]); + + return _class3; + }(); // aspect ratio + + + svg.AspectRatio = function (ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX, minY, refX, refY) { + // aspect ratio - https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + aspectRatio = svg.compressSpaces(aspectRatio); + aspectRatio = aspectRatio.replace(/^defer\s/, ''); // ignore defer + + var align = aspectRatio.split(' ')[0] || 'xMidYMid'; + var meetOrSlice = aspectRatio.split(' ')[1] || 'meet'; // calculate scale + + var scaleX = width / desiredWidth; + var scaleY = height / desiredHeight; + var scaleMin = Math.min(scaleX, scaleY); + var scaleMax = Math.max(scaleX, scaleY); + + if (meetOrSlice === 'meet') { + desiredWidth *= scaleMin; + desiredHeight *= scaleMin; + } + + if (meetOrSlice === 'slice') { + desiredWidth *= scaleMax; + desiredHeight *= scaleMax; + } + + refX = new svg.Property('refX', refX); + refY = new svg.Property('refY', refY); + + if (refX.hasValue() && refY.hasValue()) { + ctx.translate(-scaleMin * refX.toPixels('x'), -scaleMin * refY.toPixels('y')); + } else { + // align + if (align.startsWith('xMid') && (meetOrSlice === 'meet' && scaleMin === scaleY || meetOrSlice === 'slice' && scaleMax === scaleY)) ctx.translate(width / 2.0 - desiredWidth / 2.0, 0); + if (align.endsWith('YMid') && (meetOrSlice === 'meet' && scaleMin === scaleX || meetOrSlice === 'slice' && scaleMax === scaleX)) ctx.translate(0, height / 2.0 - desiredHeight / 2.0); + if (align.startsWith('xMax') && (meetOrSlice === 'meet' && scaleMin === scaleY || meetOrSlice === 'slice' && scaleMax === scaleY)) ctx.translate(width - desiredWidth, 0); + if (align.endsWith('YMax') && (meetOrSlice === 'meet' && scaleMin === scaleX || meetOrSlice === 'slice' && scaleMax === scaleX)) ctx.translate(0, height - desiredHeight); + } // scale + + + if (align === 'none') ctx.scale(scaleX, scaleY);else if (meetOrSlice === 'meet') ctx.scale(scaleMin, scaleMin);else if (meetOrSlice === 'slice') ctx.scale(scaleMax, scaleMax); // translate + + ctx.translate(isNullish(minX) ? 0 : -minX, isNullish(minY) ? 0 : -minY); + }; // elements + + + svg.Element = {}; + svg.EmptyProperty = new svg.Property('EMPTY', ''); + + svg.Element.ElementBase = + /*#__PURE__*/ + function () { + function _class4(node) { + var _this7 = this; + + _classCallCheck(this, _class4); + + // Argument from inheriting class + this.captureTextNodes = arguments[1]; // eslint-disable-line prefer-rest-params + + this.attributes = {}; + this.styles = {}; + this.children = []; + + if (!isNullish(node) && node.nodeType === 1) { + // ELEMENT_NODE + // add children + _toConsumableArray(node.childNodes).forEach(function (childNode) { + if (childNode.nodeType === 1) { + _this7.addChild(childNode, true); // ELEMENT_NODE + + } + + if (_this7.captureTextNodes && (childNode.nodeType === 3 || childNode.nodeType === 4)) { + var text = childNode.nodeValue || childNode.text || ''; + + if (svg.trim(svg.compressSpaces(text)) !== '') { + _this7.addChild(new svg.Element.tspan(childNode), false); // TEXT_NODE + + } + } + }); // add attributes + + + _toConsumableArray(node.attributes).forEach(function (_ref) { + var nodeName = _ref.nodeName, + nodeValue = _ref.nodeValue; + _this7.attributes[nodeName] = new svg.Property(nodeName, nodeValue); + }); // add tag styles + + + var styles = svg.Styles[node.nodeName]; + + if (!isNullish(styles)) { + Object.entries(styles).forEach(function (_ref2) { + var _ref3 = _slicedToArray(_ref2, 2), + name = _ref3[0], + styleValue = _ref3[1]; + + _this7.styles[name] = styleValue; + }); + } // add class styles + + + if (this.attribute('class').hasValue()) { + var classes = svg.compressSpaces(this.attribute('class').value).split(' '); + classes.forEach(function (clss) { + styles = svg.Styles['.' + clss]; + + if (!isNullish(styles)) { + Object.entries(styles).forEach(function (_ref4) { + var _ref5 = _slicedToArray(_ref4, 2), + name = _ref5[0], + styleValue = _ref5[1]; + + _this7.styles[name] = styleValue; + }); + } + + styles = svg.Styles[node.nodeName + '.' + clss]; + + if (!isNullish(styles)) { + Object.entries(styles).forEach(function (_ref6) { + var _ref7 = _slicedToArray(_ref6, 2), + name = _ref7[0], + styleValue = _ref7[1]; + + _this7.styles[name] = styleValue; + }); + } + }); + } // add id styles + + + if (this.attribute('id').hasValue()) { + var _styles = svg.Styles['#' + this.attribute('id').value]; + + if (!isNullish(_styles)) { + Object.entries(_styles).forEach(function (_ref8) { + var _ref9 = _slicedToArray(_ref8, 2), + name = _ref9[0], + styleValue = _ref9[1]; + + _this7.styles[name] = styleValue; + }); + } + } // add inline styles + + + if (this.attribute('style').hasValue()) { + var _styles2 = this.attribute('style').value.split(';'); + + _styles2.forEach(function (style) { + if (svg.trim(style) !== '') { + var _style$split = style.split(':'), + name = _style$split.name, + value = _style$split.value; + + name = svg.trim(name); + value = svg.trim(value); + _this7.styles[name] = new svg.Property(name, value); + } + }); + } // add id + + + if (this.attribute('id').hasValue()) { + if (isNullish(svg.Definitions[this.attribute('id').value])) { + svg.Definitions[this.attribute('id').value] = this; + } + } + } + } // get or create attribute + + + _createClass(_class4, [{ + key: "attribute", + value: function attribute(name, createIfNotExists) { + var a = this.attributes[name]; + if (!isNullish(a)) return a; + + if (createIfNotExists === true) { + a = new svg.Property(name, ''); + this.attributes[name] = a; + } + + return a || svg.EmptyProperty; + } + }, { + key: "getHrefAttribute", + value: function getHrefAttribute() { + for (var a in this.attributes) { + if (a.endsWith(':href')) { + return this.attributes[a]; + } + } + + return svg.EmptyProperty; + } // get or create style, crawls up node tree + + }, { + key: "style", + value: function style(name, createIfNotExists, skipAncestors) { + var s = this.styles[name]; + if (!isNullish(s)) return s; + var a = this.attribute(name); + + if (!isNullish(a) && a.hasValue()) { + this.styles[name] = a; // move up to me to cache + + return a; + } + + if (skipAncestors !== true) { + var p = this.parent; + + if (!isNullish(p)) { + var ps = p.style(name); + + if (!isNullish(ps) && ps.hasValue()) { + return ps; + } + } + } + + if (createIfNotExists === true) { + s = new svg.Property(name, ''); + this.styles[name] = s; + } + + return s || svg.EmptyProperty; + } // base render + + }, { + key: "render", + value: function render(ctx) { + // don't render display=none + if (this.style('display').value === 'none') return; // don't render visibility=hidden + + if (this.style('visibility').value === 'hidden') return; + ctx.save(); + + if (this.attribute('mask').hasValue()) { + // mask + var mask = this.attribute('mask').getDefinition(); + if (!isNullish(mask)) mask.apply(ctx, this); + } else if (this.style('filter').hasValue()) { + // filter + var filter = this.style('filter').getDefinition(); + if (!isNullish(filter)) filter.apply(ctx, this); + } else { + this.setContext(ctx); + this.renderChildren(ctx); + this.clearContext(ctx); + } + + ctx.restore(); + } // base set context + + }, { + key: "setContext", + value: function setContext(ctx) {} // OVERRIDE ME! + // base clear context + + }, { + key: "clearContext", + value: function clearContext(ctx) {} // OVERRIDE ME! + // base render children + + }, { + key: "renderChildren", + value: function renderChildren(ctx) { + this.children.forEach(function (child) { + child.render(ctx); + }); + } + }, { + key: "addChild", + value: function addChild(childNode, create) { + var child = create ? svg.CreateElement(childNode) : childNode; + child.parent = this; + + if (child.type !== 'title') { + this.children.push(child); + } + } + }]); + + return _class4; + }(); + + svg.Element.RenderedElementBase = + /*#__PURE__*/ + function (_svg$Element$ElementB) { + _inherits(_class5, _svg$Element$ElementB); + + function _class5() { + _classCallCheck(this, _class5); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class5).apply(this, arguments)); + } + + _createClass(_class5, [{ + key: "setContext", + value: function setContext(ctx) { + // fill + if (this.style('fill').isUrlDefinition()) { + var fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity')); + if (!isNullish(fs)) ctx.fillStyle = fs; + } else if (this.style('fill').hasValue()) { + var fillStyle = this.style('fill'); + if (fillStyle.value === 'currentColor') fillStyle.value = this.style('color').value; + ctx.fillStyle = fillStyle.value === 'none' ? 'rgba(0,0,0,0)' : fillStyle.value; + } + + if (this.style('fill-opacity').hasValue()) { + var _fillStyle = new svg.Property('fill', ctx.fillStyle); + + _fillStyle = _fillStyle.addOpacity(this.style('fill-opacity')); + ctx.fillStyle = _fillStyle.value; + } // stroke + + + if (this.style('stroke').isUrlDefinition()) { + var _fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity')); + + if (!isNullish(_fs)) ctx.strokeStyle = _fs; + } else if (this.style('stroke').hasValue()) { + var strokeStyle = this.style('stroke'); + if (strokeStyle.value === 'currentColor') strokeStyle.value = this.style('color').value; + ctx.strokeStyle = strokeStyle.value === 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value; + } + + if (this.style('stroke-opacity').hasValue()) { + var _strokeStyle = new svg.Property('stroke', ctx.strokeStyle); + + _strokeStyle = _strokeStyle.addOpacity(this.style('stroke-opacity')); + ctx.strokeStyle = _strokeStyle.value; + } + + if (this.style('stroke-width').hasValue()) { + var newLineWidth = this.style('stroke-width').toPixels(); + ctx.lineWidth = newLineWidth === 0 ? 0.001 : newLineWidth; // browsers don't respect 0 + } + + if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value; + if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value; + if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value; + + if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value !== 'none') { + var gaps = svg.ToNumberArray(this.style('stroke-dasharray').value); + + if (typeof ctx.setLineDash !== 'undefined') { + ctx.setLineDash(gaps); + } else if (typeof ctx.webkitLineDash !== 'undefined') { + ctx.webkitLineDash = gaps; + } else if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) { + ctx.mozDash = gaps; + } + + var offset = this.style('stroke-dashoffset').numValueOrDefault(1); + + if (typeof ctx.lineDashOffset !== 'undefined') { + ctx.lineDashOffset = offset; + } else if (typeof ctx.webkitLineDashOffset !== 'undefined') { + ctx.webkitLineDashOffset = offset; + } else if (typeof ctx.mozDashOffset !== 'undefined') { + ctx.mozDashOffset = offset; + } + } // font + + + if (typeof ctx.font !== 'undefined') { + ctx.font = svg.Font.CreateFont(this.style('font-style').value, this.style('font-variant').value, this.style('font-weight').value, this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '', this.style('font-family').value).toString(); + } // transform + + + if (this.attribute('transform').hasValue()) { + var transform = new svg.Transform(this.attribute('transform').value); + transform.apply(ctx); + } // clip + + + if (this.style('clip-path', false, true).hasValue()) { + var clip = this.style('clip-path', false, true).getDefinition(); + if (!isNullish(clip)) clip.apply(ctx); + } // opacity + + + if (this.style('opacity').hasValue()) { + ctx.globalAlpha = this.style('opacity').numValue(); + } + } + }]); + + return _class5; + }(svg.Element.ElementBase); + + svg.Element.PathElementBase = + /*#__PURE__*/ + function (_svg$Element$Rendered) { + _inherits(_class6, _svg$Element$Rendered); + + function _class6() { + _classCallCheck(this, _class6); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class6).apply(this, arguments)); + } + + _createClass(_class6, [{ + key: "path", + value: function path(ctx) { + if (!isNullish(ctx)) ctx.beginPath(); + return new svg.BoundingBox(); + } + }, { + key: "renderChildren", + value: function renderChildren(ctx) { + this.path(ctx); + svg.Mouse.checkPath(this, ctx); + + if (ctx.fillStyle !== '') { + if (this.style('fill-rule').valueOrDefault('inherit') !== 'inherit') { + ctx.fill(this.style('fill-rule').value); + } else { + ctx.fill(); + } + } + + if (ctx.strokeStyle !== '') ctx.stroke(); + var markers = this.getMarkers(); + + if (!isNullish(markers)) { + if (this.style('marker-start').isUrlDefinition()) { + var marker = this.style('marker-start').getDefinition(); + marker.render(ctx, markers[0][0], markers[0][1]); + } + + if (this.style('marker-mid').isUrlDefinition()) { + var _marker = this.style('marker-mid').getDefinition(); + + for (var i = 1; i < markers.length - 1; i++) { + _marker.render(ctx, markers[i][0], markers[i][1]); + } + } + + if (this.style('marker-end').isUrlDefinition()) { + var _marker2 = this.style('marker-end').getDefinition(); + + _marker2.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]); + } + } + } + }, { + key: "getBoundingBox", + value: function getBoundingBox() { + return this.path(); + } + }, { + key: "getMarkers", + value: function getMarkers() { + return null; + } + }]); + + return _class6; + }(svg.Element.RenderedElementBase); // svg element + + + svg.Element.svg = + /*#__PURE__*/ + function (_svg$Element$Rendered2) { + _inherits(_class7, _svg$Element$Rendered2); + + function _class7() { + _classCallCheck(this, _class7); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class7).apply(this, arguments)); + } + + _createClass(_class7, [{ + key: "clearContext", + value: function clearContext(ctx) { + _get(_getPrototypeOf(_class7.prototype), "clearContext", this).call(this, ctx); + + svg.ViewPort.RemoveCurrent(); + } + }, { + key: "setContext", + value: function setContext(ctx) { + // initial values and defaults + ctx.strokeStyle = 'rgba(0,0,0,0)'; + ctx.lineCap = 'butt'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 4; + + if (typeof ctx.font !== 'undefined' && typeof window.getComputedStyle !== 'undefined') { + ctx.font = window.getComputedStyle(ctx.canvas).getPropertyValue('font'); + } + + _get(_getPrototypeOf(_class7.prototype), "setContext", this).call(this, ctx); // create new view port + + + if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0; + if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0; + ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y')); + var width = svg.ViewPort.width(); + var height = svg.ViewPort.height(); + if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%'; + if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%'; + + if (typeof this.root === 'undefined') { + width = this.attribute('width').toPixels('x'); + height = this.attribute('height').toPixels('y'); + var x = 0; + var y = 0; + + if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) { + x = -this.attribute('refX').toPixels('x'); + y = -this.attribute('refY').toPixels('y'); + } + + if (this.attribute('overflow').valueOrDefault('hidden') !== 'visible') { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(width, y); + ctx.lineTo(width, height); + ctx.lineTo(x, height); + ctx.closePath(); + ctx.clip(); + } + } + + svg.ViewPort.SetCurrent(width, height); // viewbox + + if (this.attribute('viewBox').hasValue()) { + var viewBox = svg.ToNumberArray(this.attribute('viewBox').value); + var minX = viewBox[0]; + var minY = viewBox[1]; + width = viewBox[2]; + height = viewBox[3]; + svg.AspectRatio(ctx, this.attribute('preserveAspectRatio').value, svg.ViewPort.width(), width, svg.ViewPort.height(), height, minX, minY, this.attribute('refX').value, this.attribute('refY').value); + svg.ViewPort.RemoveCurrent(); + svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); + } + } + }]); + + return _class7; + }(svg.Element.RenderedElementBase); // rect element + + + svg.Element.rect = + /*#__PURE__*/ + function (_svg$Element$PathElem) { + _inherits(_class8, _svg$Element$PathElem); + + function _class8() { + _classCallCheck(this, _class8); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class8).apply(this, arguments)); + } + + _createClass(_class8, [{ + key: "path", + value: function path(ctx) { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + var rx = this.attribute('rx').toPixels('x'); + var ry = this.attribute('ry').toPixels('y'); + if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx; + if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry; + rx = Math.min(rx, width / 2.0); + ry = Math.min(ry, height / 2.0); + + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.moveTo(x + rx, y); + ctx.lineTo(x + width - rx, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + ry); + ctx.lineTo(x + width, y + height - ry); + ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); + ctx.lineTo(x + rx, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - ry); + ctx.lineTo(x, y + ry); + ctx.quadraticCurveTo(x, y, x + rx, y); + ctx.closePath(); + } + + return new svg.BoundingBox(x, y, x + width, y + height); + } + }]); + + return _class8; + }(svg.Element.PathElementBase); // circle element + + + svg.Element.circle = + /*#__PURE__*/ + function (_svg$Element$PathElem2) { + _inherits(_class9, _svg$Element$PathElem2); + + function _class9() { + _classCallCheck(this, _class9); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class9).apply(this, arguments)); + } + + _createClass(_class9, [{ + key: "path", + value: function path(ctx) { + var cx = this.attribute('cx').toPixels('x'); + var cy = this.attribute('cy').toPixels('y'); + var r = this.attribute('r').toPixels(); + + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2, true); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r); + } + }]); + + return _class9; + }(svg.Element.PathElementBase); // ellipse element + + + var KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + + svg.Element.ellipse = + /*#__PURE__*/ + function (_svg$Element$PathElem3) { + _inherits(_class10, _svg$Element$PathElem3); + + function _class10() { + _classCallCheck(this, _class10); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class10).apply(this, arguments)); + } + + _createClass(_class10, [{ + key: "path", + value: function path(ctx) { + var rx = this.attribute('rx').toPixels('x'); + var ry = this.attribute('ry').toPixels('y'); + var cx = this.attribute('cx').toPixels('x'); + var cy = this.attribute('cy').toPixels('y'); + + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.moveTo(cx, cy - ry); + ctx.bezierCurveTo(cx + KAPPA * rx, cy - ry, cx + rx, cy - KAPPA * ry, cx + rx, cy); + ctx.bezierCurveTo(cx + rx, cy + KAPPA * ry, cx + KAPPA * rx, cy + ry, cx, cy + ry); + ctx.bezierCurveTo(cx - KAPPA * rx, cy + ry, cx - rx, cy + KAPPA * ry, cx - rx, cy); + ctx.bezierCurveTo(cx - rx, cy - KAPPA * ry, cx - KAPPA * rx, cy - ry, cx, cy - ry); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); + } + }]); + + return _class10; + }(svg.Element.PathElementBase); // line element + + + svg.Element.line = + /*#__PURE__*/ + function (_svg$Element$PathElem4) { + _inherits(_class11, _svg$Element$PathElem4); + + function _class11() { + _classCallCheck(this, _class11); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class11).apply(this, arguments)); + } + + _createClass(_class11, [{ + key: "getPoints", + value: function getPoints() { + return [new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')), new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y'))]; + } + }, { + key: "path", + value: function path(ctx) { + var points = this.getPoints(); + + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + ctx.lineTo(points[1].x, points[1].y); + } + + return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y); + } + }, { + key: "getMarkers", + value: function getMarkers() { + var points = this.getPoints(); + var a = points[0].angleTo(points[1]); + return [[points[0], a], [points[1], a]]; + } + }]); + + return _class11; + }(svg.Element.PathElementBase); // polyline element + + + svg.Element.polyline = + /*#__PURE__*/ + function (_svg$Element$PathElem5) { + _inherits(_class12, _svg$Element$PathElem5); + + function _class12(node) { + var _this8; + + _classCallCheck(this, _class12); + + _this8 = _possibleConstructorReturn(this, _getPrototypeOf(_class12).call(this, node)); + _this8.points = svg.CreatePath(_this8.attribute('points').value); + return _this8; + } + + _createClass(_class12, [{ + key: "path", + value: function path(ctx) { + var _this$points$ = this.points[0], + x = _this$points$.x, + y = _this$points$.y; + var bb = new svg.BoundingBox(x, y); + + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.moveTo(x, y); + } + + for (var i = 1; i < this.points.length; i++) { + var _this$points$i = this.points[i], + _x = _this$points$i.x, + _y = _this$points$i.y; + bb.addPoint(_x, _y); + if (!isNullish(ctx)) ctx.lineTo(_x, _y); + } + + return bb; + } + }, { + key: "getMarkers", + value: function getMarkers() { + var markers = []; + + for (var i = 0; i < this.points.length - 1; i++) { + markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]); + } + + markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]); + return markers; + } + }]); + + return _class12; + }(svg.Element.PathElementBase); // polygon element + + + svg.Element.polygon = + /*#__PURE__*/ + function (_svg$Element$polyline) { + _inherits(_class13, _svg$Element$polyline); + + function _class13() { + _classCallCheck(this, _class13); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class13).apply(this, arguments)); + } + + _createClass(_class13, [{ + key: "path", + value: function path(ctx) { + var bb = _get(_getPrototypeOf(_class13.prototype), "path", this).call(this, ctx); + + if (!isNullish(ctx)) { + ctx.lineTo(this.points[0].x, this.points[0].y); + ctx.closePath(); + } + + return bb; + } + }]); + + return _class13; + }(svg.Element.polyline); // path element + + + svg.Element.path = + /*#__PURE__*/ + function (_svg$Element$PathElem6) { + _inherits(_class14, _svg$Element$PathElem6); + + function _class14(node) { + var _this9; + + _classCallCheck(this, _class14); + + _this9 = _possibleConstructorReturn(this, _getPrototypeOf(_class14).call(this, node)); + + var d = _this9.attribute('d').value // TODO: convert to real lexer based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF + .replace(/,/gm, ' ') // get rid of all commas + .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands + .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands + .replace(/([MmZzLlHhVvCcSsQqTtAa])([^\s])/gm, '$1 $2') // separate commands from points + .replace(/([^\s])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from points + .replace(/(\d)([+-])/gm, '$1 $2') // separate digits when no comma + .replace(/(\.\d*)(\.)/gm, '$1 $2') // separate digits when no comma + .replace(/([Aa](\s+\d+)(\s+\d+)(\s+\d+))\s+([01])\s*([01])/gm, '$1 $5 $6 '); // shorthand elliptical arc path syntax + + + d = svg.compressSpaces(d); // compress multiple spaces + + d = svg.trim(d); + _this9.PathParser = { + tokens: d.split(' '), + reset: function reset() { + this.i = -1; + this.command = ''; + this.previousCommand = ''; + this.start = new svg.Point(0, 0); + this.control = new svg.Point(0, 0); + this.current = new svg.Point(0, 0); + this.points = []; + this.angles = []; + }, + isEnd: function isEnd() { + return this.i >= this.tokens.length - 1; + }, + isCommandOrEnd: function isCommandOrEnd() { + if (this.isEnd()) return true; + return !isNullish(this.tokens[this.i + 1].match(/^[A-Za-z]$/)); + }, + isRelativeCommand: function isRelativeCommand() { + switch (this.command) { + case 'm': + case 'l': + case 'h': + case 'v': + case 'c': + case 's': + case 'q': + case 't': + case 'a': + case 'z': + return true; + } + + return false; + }, + getToken: function getToken() { + this.i++; + return this.tokens[this.i]; + }, + getScalar: function getScalar() { + return parseFloat(this.getToken()); + }, + nextCommand: function nextCommand() { + this.previousCommand = this.command; + this.command = this.getToken(); + }, + getPoint: function getPoint() { + var p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + }, + getAsControlPoint: function getAsControlPoint() { + var p = this.getPoint(); + this.control = p; + return p; + }, + getAsCurrentPoint: function getAsCurrentPoint() { + var p = this.getPoint(); + this.current = p; + return p; + }, + getReflectedControlPoint: function getReflectedControlPoint() { + if (this.previousCommand.toLowerCase() !== 'c' && this.previousCommand.toLowerCase() !== 's' && this.previousCommand.toLowerCase() !== 'q' && this.previousCommand.toLowerCase() !== 't') { + return this.current; + } // reflect point + + + var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + }, + makeAbsolute: function makeAbsolute(p) { + if (this.isRelativeCommand()) { + p.x += this.current.x; + p.y += this.current.y; + } + + return p; + }, + addMarker: function addMarker(p, from, priorTo) { + // if the last angle isn't filled in because we didn't have this point yet ... + if (!isNullish(priorTo) && this.angles.length > 0 && isNullish(this.angles[this.angles.length - 1])) { + this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo); + } + + this.addMarkerAngle(p, isNullish(from) ? null : from.angleTo(p)); + }, + addMarkerAngle: function addMarkerAngle(p, a) { + this.points.push(p); + this.angles.push(a); + }, + getMarkerPoints: function getMarkerPoints() { + return this.points; + }, + getMarkerAngles: function getMarkerAngles() { + for (var i = 0; i < this.angles.length; i++) { + if (isNullish(this.angles[i])) { + for (var j = i + 1; j < this.angles.length; j++) { + if (!isNullish(this.angles[j])) { + this.angles[i] = this.angles[j]; + break; + } + } + } + } + + return this.angles; + } + }; + return _this9; + } + + _createClass(_class14, [{ + key: "path", + value: function path(ctx) { + var pp = this.PathParser; + pp.reset(); + var bb = new svg.BoundingBox(); + if (!isNullish(ctx)) ctx.beginPath(); + + while (!pp.isEnd()) { + pp.nextCommand(); + + switch (pp.command) { + case 'M': + case 'm': + { + var p = pp.getAsCurrentPoint(); + pp.addMarker(p); + bb.addPoint(p.x, p.y); + if (!isNullish(ctx)) ctx.moveTo(p.x, p.y); + pp.start = pp.current; + + while (!pp.isCommandOrEnd()) { + var _p = pp.getAsCurrentPoint(); + + pp.addMarker(_p, pp.start); + bb.addPoint(_p.x, _p.y); + if (!isNullish(ctx)) ctx.lineTo(_p.x, _p.y); + } + + break; + } + + case 'L': + case 'l': + while (!pp.isCommandOrEnd()) { + var c = pp.current; + + var _p2 = pp.getAsCurrentPoint(); + + pp.addMarker(_p2, c); + bb.addPoint(_p2.x, _p2.y); + if (!isNullish(ctx)) ctx.lineTo(_p2.x, _p2.y); + } + + break; + + case 'H': + case 'h': + while (!pp.isCommandOrEnd()) { + var newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (!isNullish(ctx)) ctx.lineTo(pp.current.x, pp.current.y); + } + + break; + + case 'V': + case 'v': + while (!pp.isCommandOrEnd()) { + var _newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); + + pp.addMarker(_newP, pp.current); + pp.current = _newP; + bb.addPoint(pp.current.x, pp.current.y); + if (!isNullish(ctx)) ctx.lineTo(pp.current.x, pp.current.y); + } + + break; + + case 'C': + case 'c': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var p1 = pp.getPoint(); + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (!isNullish(ctx)) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + + break; + + case 'S': + case 's': + while (!pp.isCommandOrEnd()) { + var _curr = pp.current; + + var _p3 = pp.getReflectedControlPoint(); + + var _cntrl = pp.getAsControlPoint(); + + var _cp = pp.getAsCurrentPoint(); + + pp.addMarker(_cp, _cntrl, _p3); + bb.addBezierCurve(_curr.x, _curr.y, _p3.x, _p3.y, _cntrl.x, _cntrl.y, _cp.x, _cp.y); + if (!isNullish(ctx)) ctx.bezierCurveTo(_p3.x, _p3.y, _cntrl.x, _cntrl.y, _cp.x, _cp.y); + } + + break; + + case 'Q': + case 'q': + while (!pp.isCommandOrEnd()) { + var _curr2 = pp.current; + + var _cntrl2 = pp.getAsControlPoint(); + + var _cp2 = pp.getAsCurrentPoint(); + + pp.addMarker(_cp2, _cntrl2, _cntrl2); + bb.addQuadraticCurve(_curr2.x, _curr2.y, _cntrl2.x, _cntrl2.y, _cp2.x, _cp2.y); + if (!isNullish(ctx)) ctx.quadraticCurveTo(_cntrl2.x, _cntrl2.y, _cp2.x, _cp2.y); + } + + break; + + case 'T': + case 't': + while (!pp.isCommandOrEnd()) { + var _curr3 = pp.current; + + var _cntrl3 = pp.getReflectedControlPoint(); + + pp.control = _cntrl3; + + var _cp3 = pp.getAsCurrentPoint(); + + pp.addMarker(_cp3, _cntrl3, _cntrl3); + bb.addQuadraticCurve(_curr3.x, _curr3.y, _cntrl3.x, _cntrl3.y, _cp3.x, _cp3.y); + if (!isNullish(ctx)) ctx.quadraticCurveTo(_cntrl3.x, _cntrl3.y, _cp3.x, _cp3.y); + } + + break; + + case 'A': + case 'a': + var _loop2 = function _loop2() { + var curr = pp.current; + var rx = pp.getScalar(); + var ry = pp.getScalar(); + var xAxisRotation = pp.getScalar() * (Math.PI / 180.0); + var largeArcFlag = pp.getScalar(); + var sweepFlag = pp.getScalar(); + var cp = pp.getAsCurrentPoint(); // Conversion from endpoint to center parameterization + // https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter + // x1', y1' + + var currp = new svg.Point(Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0); // adjust radii + + var l = Math.pow(currp.x, 2) / Math.pow(rx, 2) + Math.pow(currp.y, 2) / Math.pow(ry, 2); + + if (l > 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } // cx', cy' + + + var s = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt((Math.pow(rx, 2) * Math.pow(ry, 2) - Math.pow(rx, 2) * Math.pow(currp.y, 2) - Math.pow(ry, 2) * Math.pow(currp.x, 2)) / (Math.pow(rx, 2) * Math.pow(currp.y, 2) + Math.pow(ry, 2) * Math.pow(currp.x, 2))); + if (isNaN(s)) s = 0; + var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); // cx, cy + + var centp = new svg.Point((curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y); // vector magnitude + + var m = function m(v) { + return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); + }; // ratio between two vectors + + + var r = function r(u, v) { + return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)); + }; // angle between two vectors + + + var a = function a(u, v) { + return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); + }; // initial angle + + + var a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]); // angle delta + + var u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]; + var v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry]; + var ad = a(u, v); + if (r(u, v) <= -1) ad = Math.PI; + if (r(u, v) >= 1) ad = 0; // for markers + + var dir = 1 - sweepFlag ? 1.0 : -1.0; + var ah = a1 + dir * (ad / 2.0); + var halfWay = new svg.Point(centp.x + rx * Math.cos(ah), centp.y + ry * Math.sin(ah)); + pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); + pp.addMarkerAngle(cp, ah - dir * Math.PI); + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + + if (!isNullish(ctx)) { + var _r = rx > ry ? rx : ry; + + var sx = rx > ry ? 1 : rx / ry; + var sy = rx > ry ? ry / rx : 1; + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, _r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1 / sx, 1 / sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + }; + + while (!pp.isCommandOrEnd()) { + _loop2(); + } + + break; + + case 'Z': + case 'z': + if (!isNullish(ctx)) ctx.closePath(); + pp.current = pp.start; + } + } + + return bb; + } + }, { + key: "getMarkers", + value: function getMarkers() { + var points = this.PathParser.getMarkerPoints(); + var angles = this.PathParser.getMarkerAngles(); + var markers = points.map(function (point, i) { + return [point, angles[i]]; + }); + return markers; + } + }]); + + return _class14; + }(svg.Element.PathElementBase); // pattern element + + + svg.Element.pattern = + /*#__PURE__*/ + function (_svg$Element$ElementB2) { + _inherits(_class15, _svg$Element$ElementB2); + + function _class15() { + _classCallCheck(this, _class15); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class15).apply(this, arguments)); + } + + _createClass(_class15, [{ + key: "createPattern", + value: function createPattern(ctx, element) { + var width = this.attribute('width').toPixels('x', true); + var height = this.attribute('height').toPixels('y', true); // render me using a temporary svg element + + var tempSvg = new svg.Element.svg(); + tempSvg.attributes.viewBox = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes.width = new svg.Property('width', width + 'px'); + tempSvg.attributes.height = new svg.Property('height', height + 'px'); + tempSvg.attributes.transform = new svg.Property('transform', this.attribute('patternTransform').value); + tempSvg.children = this.children; + var c = document.createElement('canvas'); + c.width = width; + c.height = height; + var cctx = c.getContext('2d'); + + if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) { + cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true)); + } // render 3x3 grid so when we transform there's no white space on edges + + + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + cctx.save(); + cctx.translate(x * c.width, y * c.height); + tempSvg.render(cctx); + cctx.restore(); + } + } + + var pattern = ctx.createPattern(c, 'repeat'); + return pattern; + } + }]); + + return _class15; + }(svg.Element.ElementBase); // marker element + + + svg.Element.marker = + /*#__PURE__*/ + function (_svg$Element$ElementB3) { + _inherits(_class16, _svg$Element$ElementB3); + + function _class16() { + _classCallCheck(this, _class16); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class16).apply(this, arguments)); + } + + _createClass(_class16, [{ + key: "render", + value: function render(ctx, point, angle) { + ctx.translate(point.x, point.y); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(angle); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth); + ctx.save(); // render me using a temporary svg element + + var tempSvg = new svg.Element.svg(); + tempSvg.attributes.viewBox = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes.refX = new svg.Property('refX', this.attribute('refX').value); + tempSvg.attributes.refY = new svg.Property('refY', this.attribute('refY').value); + tempSvg.attributes.width = new svg.Property('width', this.attribute('markerWidth').value); + tempSvg.attributes.height = new svg.Property('height', this.attribute('markerHeight').value); + tempSvg.attributes.fill = new svg.Property('fill', this.attribute('fill').valueOrDefault('black')); + tempSvg.attributes.stroke = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none')); + tempSvg.children = this.children; + tempSvg.render(ctx); + ctx.restore(); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(-angle); + ctx.translate(-point.x, -point.y); + } + }]); + + return _class16; + }(svg.Element.ElementBase); // definitions element + + + svg.Element.defs = + /*#__PURE__*/ + function (_svg$Element$ElementB4) { + _inherits(_class17, _svg$Element$ElementB4); + + function _class17() { + _classCallCheck(this, _class17); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class17).apply(this, arguments)); + } + + _createClass(_class17, [{ + key: "render", + value: function render(ctx) {// NOOP + } + }]); + + return _class17; + }(svg.Element.ElementBase); // base for gradients + + + svg.Element.GradientBase = + /*#__PURE__*/ + function (_svg$Element$ElementB5) { + _inherits(_class18, _svg$Element$ElementB5); + + function _class18(node) { + var _this10; + + _classCallCheck(this, _class18); + + _this10 = _possibleConstructorReturn(this, _getPrototypeOf(_class18).call(this, node)); + _this10.gradientUnits = _this10.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); + _this10.stops = []; + + _this10.children.forEach(function (child) { + if (child.type === 'stop') { + _this10.stops.push(child); + } + }); + + return _this10; + } + + _createClass(_class18, [{ + key: "getGradient", + value: function getGradient() {// OVERRIDE ME! + } + }, { + key: "createGradient", + value: function createGradient(ctx, element, parentOpacityProp) { + var stopsContainer = this.getHrefAttribute().hasValue() ? this.getHrefAttribute().getDefinition() : this; + + var addParentOpacity = function addParentOpacity(color) { + if (parentOpacityProp.hasValue()) { + var p = new svg.Property('color', color); + return p.addOpacity(parentOpacityProp).value; + } + + return color; + }; + + var g = this.getGradient(ctx, element); + if (isNullish(g)) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color); + stopsContainer.stops.forEach(function (_ref10) { + var offset = _ref10.offset, + color = _ref10.color; + g.addColorStop(offset, addParentOpacity(color)); + }); + + if (this.attribute('gradientTransform').hasValue()) { + // render as transformed pattern on temporary canvas + var rootView = svg.ViewPort.viewPorts[0]; + var rect = new svg.Element.rect(); + rect.attributes.x = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes.y = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes.width = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS); + rect.attributes.height = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS); + var group = new svg.Element.g(); + group.attributes.transform = new svg.Property('transform', this.attribute('gradientTransform').value); + group.children = [rect]; + var tempSvg = new svg.Element.svg(); + tempSvg.attributes.x = new svg.Property('x', 0); + tempSvg.attributes.y = new svg.Property('y', 0); + tempSvg.attributes.width = new svg.Property('width', rootView.width); + tempSvg.attributes.height = new svg.Property('height', rootView.height); + tempSvg.children = [group]; + var c = document.createElement('canvas'); + c.width = rootView.width; + c.height = rootView.height; + var tempCtx = c.getContext('2d'); + tempCtx.fillStyle = g; + tempSvg.render(tempCtx); + return tempCtx.createPattern(c, 'no-repeat'); + } + + return g; + } + }]); + + return _class18; + }(svg.Element.ElementBase); // linear gradient element + + + svg.Element.linearGradient = + /*#__PURE__*/ + function (_svg$Element$Gradient) { + _inherits(_class19, _svg$Element$Gradient); + + function _class19() { + _classCallCheck(this, _class19); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class19).apply(this, arguments)); + } + + _createClass(_class19, [{ + key: "getGradient", + value: function getGradient(ctx, element) { + var useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; + var bb = useBB ? element.getBoundingBox() : null; + + if (!this.attribute('x1').hasValue() && !this.attribute('y1').hasValue() && !this.attribute('x2').hasValue() && !this.attribute('y2').hasValue()) { + this.attribute('x1', true).value = 0; + this.attribute('y1', true).value = 0; + this.attribute('x2', true).value = 1; + this.attribute('y2', true).value = 0; + } + + var x1 = useBB ? bb.x() + bb.width() * this.attribute('x1').numValue() : this.attribute('x1').toPixels('x'); + var y1 = useBB ? bb.y() + bb.height() * this.attribute('y1').numValue() : this.attribute('y1').toPixels('y'); + var x2 = useBB ? bb.x() + bb.width() * this.attribute('x2').numValue() : this.attribute('x2').toPixels('x'); + var y2 = useBB ? bb.y() + bb.height() * this.attribute('y2').numValue() : this.attribute('y2').toPixels('y'); + if (x1 === x2 && y1 === y2) return null; + return ctx.createLinearGradient(x1, y1, x2, y2); + } + }]); + + return _class19; + }(svg.Element.GradientBase); // radial gradient element + + + svg.Element.radialGradient = + /*#__PURE__*/ + function (_svg$Element$Gradient2) { + _inherits(_class20, _svg$Element$Gradient2); + + function _class20() { + _classCallCheck(this, _class20); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class20).apply(this, arguments)); + } + + _createClass(_class20, [{ + key: "getGradient", + value: function getGradient(ctx, element) { + var useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; + var bb = useBB ? element.getBoundingBox() : null; + if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%'; + if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%'; + if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%'; + var cx = useBB ? bb.x() + bb.width() * this.attribute('cx').numValue() : this.attribute('cx').toPixels('x'); + var cy = useBB ? bb.y() + bb.height() * this.attribute('cy').numValue() : this.attribute('cy').toPixels('y'); + var fx = cx; + var fy = cy; + + if (this.attribute('fx').hasValue()) { + fx = useBB ? bb.x() + bb.width() * this.attribute('fx').numValue() : this.attribute('fx').toPixels('x'); + } + + if (this.attribute('fy').hasValue()) { + fy = useBB ? bb.y() + bb.height() * this.attribute('fy').numValue() : this.attribute('fy').toPixels('y'); + } + + var r = useBB ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue() : this.attribute('r').toPixels(); + return ctx.createRadialGradient(fx, fy, 0, cx, cy, r); + } + }]); + + return _class20; + }(svg.Element.GradientBase); // gradient stop element + + + svg.Element.stop = + /*#__PURE__*/ + function (_svg$Element$ElementB6) { + _inherits(_class21, _svg$Element$ElementB6); + + function _class21(node) { + var _this11; + + _classCallCheck(this, _class21); + + _this11 = _possibleConstructorReturn(this, _getPrototypeOf(_class21).call(this, node)); + _this11.offset = _this11.attribute('offset').numValue(); + if (_this11.offset < 0) _this11.offset = 0; + if (_this11.offset > 1) _this11.offset = 1; + + var stopColor = _this11.style('stop-color'); + + if (_this11.style('stop-opacity').hasValue()) { + stopColor = stopColor.addOpacity(_this11.style('stop-opacity')); + } + + _this11.color = stopColor.value; + return _this11; + } + + return _class21; + }(svg.Element.ElementBase); // animation base element + + + svg.Element.AnimateBase = + /*#__PURE__*/ + function (_svg$Element$ElementB7) { + _inherits(_class22, _svg$Element$ElementB7); + + function _class22(node) { + var _this12; + + _classCallCheck(this, _class22); + + _this12 = _possibleConstructorReturn(this, _getPrototypeOf(_class22).call(this, node)); + svg.Animations.push(_assertThisInitialized(_this12)); + _this12.duration = 0.0; + _this12.begin = _this12.attribute('begin').toMilliseconds(); + _this12.maxDuration = _this12.begin + _this12.attribute('dur').toMilliseconds(); + _this12.initialValue = null; + _this12.initialUnits = ''; + _this12.removed = false; + _this12.from = _this12.attribute('from'); + _this12.to = _this12.attribute('to'); + _this12.values = _this12.attribute('values'); + if (_this12.values.hasValue()) _this12.values.value = _this12.values.value.split(';'); + return _this12; + } + + _createClass(_class22, [{ + key: "getProperty", + value: function getProperty() { + var attributeType = this.attribute('attributeType').value; + var attributeName = this.attribute('attributeName').value; + + if (attributeType === 'CSS') { + return this.parent.style(attributeName, true); + } + + return this.parent.attribute(attributeName, true); + } + }, { + key: "calcValue", + value: function calcValue() { + // OVERRIDE ME! + return ''; + } + }, { + key: "update", + value: function update(delta) { + // set initial value + if (isNullish(this.initialValue)) { + this.initialValue = this.getProperty().value; + this.initialUnits = this.getProperty().getUnits(); + } // if we're past the end time + + + if (this.duration > this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value === 'indefinite' || this.attribute('repeatDur').value === 'indefinite') { + this.duration = 0.0; + } else if (this.attribute('fill').valueOrDefault('remove') === 'freeze' && !this.frozen) { + this.frozen = true; + this.parent.animationFrozen = true; + this.parent.animationFrozenValue = this.getProperty().value; + } else if (this.attribute('fill').valueOrDefault('remove') === 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue; + return true; + } + + return false; + } + + this.duration = this.duration + delta; // if we're past the begin time + + var updated = false; + + if (this.begin < this.duration) { + var newValue = this.calcValue(); // tween + + if (this.attribute('type').hasValue()) { + // for transform, etc. + var type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } + + this.getProperty().value = newValue; + updated = true; + } + + return updated; + } // fraction of duration we've covered + + }, { + key: "progress", + value: function progress() { + var ret = { + progress: (this.duration - this.begin) / (this.maxDuration - this.begin) + }; + + if (this.values.hasValue()) { + var p = ret.progress * (this.values.value.length - 1); + var lb = Math.floor(p), + ub = Math.ceil(p); + ret.from = new svg.Property('from', parseFloat(this.values.value[lb])); + ret.to = new svg.Property('to', parseFloat(this.values.value[ub])); + ret.progress = (p - lb) / (ub - lb); + } else { + ret.from = this.from; + ret.to = this.to; + } + + return ret; + } + }]); + + return _class22; + }(svg.Element.ElementBase); // animate element + + + svg.Element.animate = + /*#__PURE__*/ + function (_svg$Element$AnimateB) { + _inherits(_class23, _svg$Element$AnimateB); + + function _class23() { + _classCallCheck(this, _class23); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class23).apply(this, arguments)); + } + + _createClass(_class23, [{ + key: "calcValue", + value: function calcValue() { + var p = this.progress(); // tween value linearly + + var newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; + return newValue + this.initialUnits; + } + }]); + + return _class23; + }(svg.Element.AnimateBase); // animate color element + + + svg.Element.animateColor = + /*#__PURE__*/ + function (_svg$Element$AnimateB2) { + _inherits(_class24, _svg$Element$AnimateB2); + + function _class24() { + _classCallCheck(this, _class24); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class24).apply(this, arguments)); + } + + _createClass(_class24, [{ + key: "calcValue", + value: function calcValue() { + var p = this.progress(); + var from = new RGBColor(p.from.value); + var to = new RGBColor(p.to.value); + + if (from.ok && to.ok) { + // tween color linearly + var r = from.r + (to.r - from.r) * p.progress; + var g = from.g + (to.g - from.g) * p.progress; + var b = from.b + (to.b - from.b) * p.progress; + return 'rgb(' + parseInt(r) + ',' + parseInt(g) + ',' + parseInt(b) + ')'; + } + + return this.attribute('from').value; + } + }]); + + return _class24; + }(svg.Element.AnimateBase); // animate transform element + + + svg.Element.animateTransform = + /*#__PURE__*/ + function (_svg$Element$animate) { + _inherits(_class25, _svg$Element$animate); + + function _class25() { + _classCallCheck(this, _class25); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class25).apply(this, arguments)); + } + + _createClass(_class25, [{ + key: "calcValue", + value: function calcValue() { + var p = this.progress(); // tween value linearly + + var from = svg.ToNumberArray(p.from.value); + var to = svg.ToNumberArray(p.to.value); + var newValue = ''; + from.forEach(function (fr, i) { + newValue += fr + (to[i] - fr) * p.progress + ' '; + }); + return newValue; + } + }]); + + return _class25; + }(svg.Element.animate); // font element + + + svg.Element.font = + /*#__PURE__*/ + function (_svg$Element$ElementB8) { + _inherits(_class26, _svg$Element$ElementB8); + + function _class26(node) { + var _this13; + + _classCallCheck(this, _class26); + + _this13 = _possibleConstructorReturn(this, _getPrototypeOf(_class26).call(this, node)); + _this13.horizAdvX = _this13.attribute('horiz-adv-x').numValue(); + _this13.isRTL = false; + _this13.isArabic = false; + _this13.fontFace = null; + _this13.missingGlyph = null; + _this13.glyphs = []; + + _this13.children.forEach(function (child) { + if (child.type === 'font-face') { + _this13.fontFace = child; + + if (child.style('font-family').hasValue()) { + svg.Definitions[child.style('font-family').value] = _assertThisInitialized(_this13); + } + } else if (child.type === 'missing-glyph') { + _this13.missingGlyph = child; + } else if (child.type === 'glyph') { + if (child.arabicForm !== '') { + _this13.isRTL = true; + _this13.isArabic = true; + + if (typeof _this13.glyphs[child.unicode] === 'undefined') { + _this13.glyphs[child.unicode] = []; + } + + _this13.glyphs[child.unicode][child.arabicForm] = child; + } else { + _this13.glyphs[child.unicode] = child; + } + } + }); + + return _this13; + } + + return _class26; + }(svg.Element.ElementBase); // font-face element + + + svg.Element.fontface = + /*#__PURE__*/ + function (_svg$Element$ElementB9) { + _inherits(_class27, _svg$Element$ElementB9); + + function _class27(node) { + var _this14; + + _classCallCheck(this, _class27); + + _this14 = _possibleConstructorReturn(this, _getPrototypeOf(_class27).call(this, node)); + _this14.ascent = _this14.attribute('ascent').value; + _this14.descent = _this14.attribute('descent').value; + _this14.unitsPerEm = _this14.attribute('units-per-em').numValue(); + return _this14; + } + + return _class27; + }(svg.Element.ElementBase); // missing-glyph element + + + svg.Element.missingglyph = + /*#__PURE__*/ + function (_svg$Element$path) { + _inherits(_class28, _svg$Element$path); + + function _class28(node) { + var _this15; + + _classCallCheck(this, _class28); + + _this15 = _possibleConstructorReturn(this, _getPrototypeOf(_class28).call(this, node)); + _this15.horizAdvX = 0; + return _this15; + } + + return _class28; + }(svg.Element.path); // glyph element + + + svg.Element.glyph = + /*#__PURE__*/ + function (_svg$Element$path2) { + _inherits(_class29, _svg$Element$path2); + + function _class29(node) { + var _this16; + + _classCallCheck(this, _class29); + + _this16 = _possibleConstructorReturn(this, _getPrototypeOf(_class29).call(this, node)); + _this16.horizAdvX = _this16.attribute('horiz-adv-x').numValue(); + _this16.unicode = _this16.attribute('unicode').value; + _this16.arabicForm = _this16.attribute('arabic-form').value; + return _this16; + } + + return _class29; + }(svg.Element.path); // text element + + + svg.Element.text = + /*#__PURE__*/ + function (_svg$Element$Rendered3) { + _inherits(_class30, _svg$Element$Rendered3); + + function _class30(node) { + _classCallCheck(this, _class30); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class30).call(this, node, true)); + } + + _createClass(_class30, [{ + key: "setContext", + value: function setContext(ctx) { + _get(_getPrototypeOf(_class30.prototype), "setContext", this).call(this, ctx); + + var textBaseline = this.style('dominant-baseline').toTextBaseline(); + if (isNullish(textBaseline)) textBaseline = this.style('alignment-baseline').toTextBaseline(); + if (!isNullish(textBaseline)) ctx.textBaseline = textBaseline; + } + }, { + key: "getBoundingBox", + value: function getBoundingBox() { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + return new svg.BoundingBox(x, y - fontSize, x + Math.floor(fontSize * 2.0 / 3.0) * this.children[0].getText().length, y); + } + }, { + key: "renderChildren", + value: function renderChildren(ctx) { + var _this17 = this; + + this.x = this.attribute('x').toPixels('x'); + this.y = this.attribute('y').toPixels('y'); + this.x += this.getAnchorDelta(ctx, this, 0); + this.children.forEach(function (child, i) { + _this17.renderChild(ctx, _this17, i); + }); + } + }, { + key: "getAnchorDelta", + value: function getAnchorDelta(ctx, parent, startI) { + var textAnchor = this.style('text-anchor').valueOrDefault('start'); + + if (textAnchor !== 'start') { + var width = 0; + + for (var i = startI; i < parent.children.length; i++) { + var child = parent.children[i]; + if (i > startI && child.attribute('x').hasValue()) break; // new group + + width += child.measureTextRecursive(ctx); + } + + return -1 * (textAnchor === 'end' ? width : width / 2.0); + } + + return 0; + } + }, { + key: "renderChild", + value: function renderChild(ctx, parent, i) { + var child = parent.children[i]; + + if (child.attribute('x').hasValue()) { + child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i); + if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x'); + } else { + if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); + if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x'); + child.x = this.x; + } + + this.x = child.x + child.measureText(ctx); + + if (child.attribute('y').hasValue()) { + child.y = child.attribute('y').toPixels('y'); + if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y'); + } else { + if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); + if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y'); + child.y = this.y; + } + + this.y = child.y; + child.render(ctx); + + for (var j = 0; j < child.children.length; j++) { + this.renderChild(ctx, child, j); + } + } + }]); + + return _class30; + }(svg.Element.RenderedElementBase); // text base + + + svg.Element.TextElementBase = + /*#__PURE__*/ + function (_svg$Element$Rendered4) { + _inherits(_class31, _svg$Element$Rendered4); + + function _class31() { + _classCallCheck(this, _class31); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class31).apply(this, arguments)); + } + + _createClass(_class31, [{ + key: "getGlyph", + value: function getGlyph(font, text, i) { + var c = text[i]; + var glyph = null; + + if (font.isArabic) { + var arabicForm = 'isolated'; + if ((i === 0 || text[i - 1] === ' ') && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'terminal'; + if (i > 0 && text[i - 1] !== ' ' && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'medial'; + if (i > 0 && text[i - 1] !== ' ' && (i === text.length - 1 || text[i + 1] === ' ')) arabicForm = 'initial'; + + if (typeof font.glyphs[c] !== 'undefined') { + glyph = font.glyphs[c][arabicForm]; + if (isNullish(glyph) && font.glyphs[c].type === 'glyph') glyph = font.glyphs[c]; + } + } else { + glyph = font.glyphs[c]; + } + + if (isNullish(glyph)) glyph = font.missingGlyph; + return glyph; + } + }, { + key: "renderChildren", + value: function renderChildren(ctx) { + var customFont = this.parent.style('font-family').getDefinition(); + + if (!isNullish(customFont)) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); + var text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + + for (var i = 0; i < text.length; i++) { + var glyph = this.getGlyph(customFont, text, i); + var scale = fontSize / customFont.fontFace.unitsPerEm; + ctx.translate(this.x, this.y); + ctx.scale(scale, -scale); + var lw = ctx.lineWidth; + ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize; + if (fontStyle === 'italic') ctx.transform(1, 0, 0.4, 1, 0, 0); + glyph.render(ctx); + if (fontStyle === 'italic') ctx.transform(1, 0, -0.4, 1, 0, 0); + ctx.lineWidth = lw; + ctx.scale(1 / scale, -1 / scale); + ctx.translate(-this.x, -this.y); + this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm; + + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + this.x += dx[i]; + } + } + + return; + } + + if (ctx.fillStyle !== '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); + if (ctx.strokeStyle !== '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y); + } + }, { + key: "getText", + value: function getText() {// OVERRIDE ME + } + }, { + key: "measureTextRecursive", + value: function measureTextRecursive(ctx) { + var width = this.measureText(ctx); + this.children.forEach(function (child) { + width += child.measureTextRecursive(ctx); + }); + return width; + } + }, { + key: "measureText", + value: function measureText(ctx) { + var customFont = this.parent.style('font-family').getDefinition(); + + if (!isNullish(customFont)) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + var measure = 0; + var text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + + for (var i = 0; i < text.length; i++) { + var glyph = this.getGlyph(customFont, text, i); + measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; + + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + measure += dx[i]; + } + } + + return measure; + } + + var textToMeasure = svg.compressSpaces(this.getText()); + if (!ctx.measureText) return textToMeasure.length * 10; + ctx.save(); + this.setContext(ctx); + + var _ctx$measureText = ctx.measureText(textToMeasure), + width = _ctx$measureText.width; + + ctx.restore(); + return width; + } + }]); + + return _class31; + }(svg.Element.RenderedElementBase); // tspan + + + svg.Element.tspan = + /*#__PURE__*/ + function (_svg$Element$TextElem) { + _inherits(_class32, _svg$Element$TextElem); + + function _class32(node) { + var _this18; + + _classCallCheck(this, _class32); + + _this18 = _possibleConstructorReturn(this, _getPrototypeOf(_class32).call(this, node, true)); + _this18.text = node.nodeValue || node.text || ''; + return _this18; + } + + _createClass(_class32, [{ + key: "getText", + value: function getText() { + return this.text; + } + }]); + + return _class32; + }(svg.Element.TextElementBase); // tref + + + svg.Element.tref = + /*#__PURE__*/ + function (_svg$Element$TextElem2) { + _inherits(_class33, _svg$Element$TextElem2); + + function _class33() { + _classCallCheck(this, _class33); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class33).apply(this, arguments)); + } + + _createClass(_class33, [{ + key: "getText", + value: function getText() { + var element = this.getHrefAttribute().getDefinition(); + if (!isNullish(element)) return element.children[0].getText(); + return undefined; + } + }]); + + return _class33; + }(svg.Element.TextElementBase); // a element + + + svg.Element.a = + /*#__PURE__*/ + function (_svg$Element$TextElem3) { + _inherits(_class34, _svg$Element$TextElem3); + + function _class34(node) { + var _this19; + + _classCallCheck(this, _class34); + + _this19 = _possibleConstructorReturn(this, _getPrototypeOf(_class34).call(this, node)); + _this19.hasText = true; + + _toConsumableArray(node.childNodes).forEach(function (childNode) { + if (childNode.nodeType !== 3) { + _this19.hasText = false; + } + }); // this might contain text + + + _this19.text = _this19.hasText ? node.childNodes[0].nodeValue : ''; + return _this19; + } + + _createClass(_class34, [{ + key: "getText", + value: function getText() { + return this.text; + } + }, { + key: "renderChildren", + value: function renderChildren(ctx) { + if (this.hasText) { + // render as text element + _get(_getPrototypeOf(_class34.prototype), "renderChildren", this).call(this, ctx); + + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.toPixels('y'), this.x + this.measureText(ctx), this.y)); + } else { + // render as temporary group + var g = new svg.Element.g(); + g.children = this.children; + g.parent = this; + g.render(ctx); + } + } + }, { + key: "onclick", + value: function onclick() { + window.open(this.getHrefAttribute().value); + } + }, { + key: "onmousemove", + value: function onmousemove() { + svg.ctx.canvas.style.cursor = 'pointer'; + } + }]); + + return _class34; + }(svg.Element.TextElementBase); // image element + + + svg.Element.image = + /*#__PURE__*/ + function (_svg$Element$Rendered5) { + _inherits(_class35, _svg$Element$Rendered5); + + function _class35(node) { + var _this20; + + _classCallCheck(this, _class35); + + _this20 = _possibleConstructorReturn(this, _getPrototypeOf(_class35).call(this, node)); + + var href = _this20.getHrefAttribute().value; + + if (href === '') { + return _possibleConstructorReturn(_this20); + } + + _this20._isSvg = href.match(/\.svg$/); + svg.Images.push(_assertThisInitialized(_this20)); + _this20.loaded = false; + + if (!_this20._isSvg) { + _this20.img = document.createElement('img'); + + if (svg.opts.useCORS === true) { + _this20.img.crossOrigin = 'Anonymous'; + } + + _this20.img.addEventListener('load', function () { + _this20.loaded = true; + }); + + _this20.img.addEventListener('error', function () { + svg.log('ERROR: image "' + href + '" not found'); + _this20.loaded = true; + }); + + _this20.img.src = href; + } else { + svg.ajax(href, true).then(function (img) { + // eslint-disable-line promise/prefer-await-to-then, promise/always-return + _this20.img = img; + _this20.loaded = true; + })["catch"](function (err) { + // eslint-disable-line promise/prefer-await-to-callbacks + _this20.erred = true; + console.error('Ajax error for canvg', err); // eslint-disable-line no-console + }); + } + + return _this20; + } + + _createClass(_class35, [{ + key: "renderChildren", + value: function renderChildren(ctx) { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + if (width === 0 || height === 0) return; + ctx.save(); + + if (this._isSvg) { + ctx.drawSvg(this.img, x, y, width, height); + } else { + ctx.translate(x, y); + svg.AspectRatio(ctx, this.attribute('preserveAspectRatio').value, width, this.img.width, height, this.img.height, 0, 0); + ctx.drawImage(this.img, 0, 0); + } + + ctx.restore(); + } + }, { + key: "getBoundingBox", + value: function getBoundingBox() { + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + return new svg.BoundingBox(x, y, x + width, y + height); + } + }]); + + return _class35; + }(svg.Element.RenderedElementBase); // group element + + + svg.Element.g = + /*#__PURE__*/ + function (_svg$Element$Rendered6) { + _inherits(_class36, _svg$Element$Rendered6); + + function _class36() { + _classCallCheck(this, _class36); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class36).apply(this, arguments)); + } + + _createClass(_class36, [{ + key: "getBoundingBox", + value: function getBoundingBox() { + var bb = new svg.BoundingBox(); + this.children.forEach(function (child) { + bb.addBoundingBox(child.getBoundingBox()); + }); + return bb; + } + }]); + + return _class36; + }(svg.Element.RenderedElementBase); // symbol element + + + svg.Element.symbol = + /*#__PURE__*/ + function (_svg$Element$Rendered7) { + _inherits(_class37, _svg$Element$Rendered7); + + function _class37() { + _classCallCheck(this, _class37); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class37).apply(this, arguments)); + } + + _createClass(_class37, [{ + key: "render", + value: function render(ctx) {// NO RENDER + } + }]); + + return _class37; + }(svg.Element.RenderedElementBase); // style element + + + svg.Element.style = + /*#__PURE__*/ + function (_svg$Element$ElementB10) { + _inherits(_class38, _svg$Element$ElementB10); + + function _class38(node) { + var _this21; + + _classCallCheck(this, _class38); + + _this21 = _possibleConstructorReturn(this, _getPrototypeOf(_class38).call(this, node)); // text, or spaces then CDATA + + var css = ''; + + _toConsumableArray(node.childNodes).forEach(function (_ref11) { + var nodeValue = _ref11.nodeValue; + css += nodeValue; + }); // remove comments + + + css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // eslint-disable-line unicorn/no-unsafe-regex + // replace whitespace + + css = svg.compressSpaces(css); + var cssDefs = css.split('}'); + cssDefs.forEach(function (cssDef) { + if (svg.trim(cssDef) !== '') { + var _cssDef$split = cssDef.split('{'), + _cssDef$split2 = _slicedToArray(_cssDef$split, 2), + cssClasses = _cssDef$split2[0], + cssProps = _cssDef$split2[1]; + + cssClasses = cssClasses.split(','); + cssProps = cssProps.split(';'); + cssClasses.forEach(function (cssClass) { + cssClass = svg.trim(cssClass); + + if (cssClass !== '') { + var props = {}; + cssProps.forEach(function (cssProp) { + var prop = cssProp.indexOf(':'); + var name = cssProp.substr(0, prop); + var value = cssProp.substr(prop + 1, cssProp.length - prop); + + if (!isNullish(name) && !isNullish(value)) { + props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value)); + } + }); + svg.Styles[cssClass] = props; + + if (cssClass === '@font-face') { + var fontFamily = props['font-family'].value.replace(/"/g, ''); + var srcs = props.src.value.split(','); + srcs.forEach(function (src) { + if (src.includes('format("svg")')) { + var urlStart = src.indexOf('url'); + var urlEnd = src.indexOf(')', urlStart); + var url = src.substr(urlStart + 5, urlEnd - urlStart - 6); // Can this ajax safely be converted to async? + + var doc = svg.parseXml(svg.ajax(url)); + var fonts = doc.getElementsByTagName('font'); + + _toConsumableArray(fonts).forEach(function (font) { + font = svg.CreateElement(font); + svg.Definitions[fontFamily] = font; + }); + } + }); + } + } + }); + } + }); + return _this21; + } + + return _class38; + }(svg.Element.ElementBase); // use element + + + svg.Element.use = + /*#__PURE__*/ + function (_svg$Element$Rendered8) { + _inherits(_class39, _svg$Element$Rendered8); + + function _class39(node) { + var _this22; + + _classCallCheck(this, _class39); + + _this22 = _possibleConstructorReturn(this, _getPrototypeOf(_class39).call(this, node)); + _this22._el = _this22.getHrefAttribute().getDefinition(); + return _this22; + } + + _createClass(_class39, [{ + key: "setContext", + value: function setContext(ctx) { + _get(_getPrototypeOf(_class39.prototype), "setContext", this).call(this, ctx); + + if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0); + if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y')); + } + }, { + key: "path", + value: function path(ctx) { + var element = this._el; + if (!isNullish(element)) element.path(ctx); + } + }, { + key: "getBoundingBox", + value: function getBoundingBox() { + var element = this._el; + if (!isNullish(element)) return element.getBoundingBox(); + return undefined; + } + }, { + key: "renderChildren", + value: function renderChildren(ctx) { + var element = this._el; + + if (!isNullish(element)) { + var tempSvg = element; + + if (element.type === 'symbol') { + // render me using a temporary svg element in symbol cases (https://www.w3.org/TR/SVG/struct.html#UseElement) + tempSvg = new svg.Element.svg(); + tempSvg.type = 'svg'; + tempSvg.attributes.viewBox = new svg.Property('viewBox', element.attribute('viewBox').value); + tempSvg.attributes.preserveAspectRatio = new svg.Property('preserveAspectRatio', element.attribute('preserveAspectRatio').value); + tempSvg.attributes.overflow = new svg.Property('overflow', element.attribute('overflow').value); + tempSvg.children = element.children; + } + + if (tempSvg.type === 'svg') { + // if symbol or svg, inherit width/height from me + if (this.attribute('width').hasValue()) tempSvg.attributes.width = new svg.Property('width', this.attribute('width').value); + if (this.attribute('height').hasValue()) tempSvg.attributes.height = new svg.Property('height', this.attribute('height').value); + } + + var oldParent = tempSvg.parent; + tempSvg.parent = null; + tempSvg.render(ctx); + tempSvg.parent = oldParent; + } + } + }]); + + return _class39; + }(svg.Element.RenderedElementBase); // mask element + + + svg.Element.mask = + /*#__PURE__*/ + function (_svg$Element$ElementB11) { + _inherits(_class40, _svg$Element$ElementB11); + + function _class40() { + _classCallCheck(this, _class40); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class40).apply(this, arguments)); + } + + _createClass(_class40, [{ + key: "apply", + value: function apply(ctx, element) { + // render as temp svg + var x = this.attribute('x').toPixels('x'); + var y = this.attribute('y').toPixels('y'); + var width = this.attribute('width').toPixels('x'); + var height = this.attribute('height').toPixels('y'); + + if (width === 0 && height === 0) { + var bb = new svg.BoundingBox(); + this.children.forEach(function (child) { + bb.addBoundingBox(child.getBoundingBox()); + }); + x = Math.floor(bb.x1); + y = Math.floor(bb.y1); + width = Math.floor(bb.width()); + height = Math.floor(bb.height()); + } // temporarily remove mask to avoid recursion + + + var mask = element.attribute('mask').value; + element.attribute('mask').value = ''; + var cMask = document.createElement('canvas'); + cMask.width = x + width; + cMask.height = y + height; + var maskCtx = cMask.getContext('2d'); + this.renderChildren(maskCtx); + var c = document.createElement('canvas'); + c.width = x + width; + c.height = y + height; + var tempCtx = c.getContext('2d'); + element.render(tempCtx); + tempCtx.globalCompositeOperation = 'destination-in'; + tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat'); + tempCtx.fillRect(0, 0, x + width, y + height); + ctx.fillStyle = tempCtx.createPattern(c, 'no-repeat'); + ctx.fillRect(0, 0, x + width, y + height); // reassign mask + + element.attribute('mask').value = mask; + } + }, { + key: "render", + value: function render(ctx) {// NO RENDER + } + }]); + + return _class40; + }(svg.Element.ElementBase); // clip element + + + svg.Element.clipPath = + /*#__PURE__*/ + function (_svg$Element$ElementB12) { + _inherits(_class41, _svg$Element$ElementB12); + + function _class41() { + _classCallCheck(this, _class41); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class41).apply(this, arguments)); + } + + _createClass(_class41, [{ + key: "apply", + value: function apply(ctx) { + this.children.forEach(function (child) { + if (typeof child.path !== 'undefined') { + var transform = null; + + if (child.attribute('transform').hasValue()) { + transform = new svg.Transform(child.attribute('transform').value); + transform.apply(ctx); + } + + child.path(ctx); + ctx.clip(); + + if (transform) { + transform.unapply(ctx); + } + } + }); + } + }, { + key: "render", + value: function render(ctx) {// NO RENDER + } + }]); + + return _class41; + }(svg.Element.ElementBase); // filters + + + svg.Element.filter = + /*#__PURE__*/ + function (_svg$Element$ElementB13) { + _inherits(_class42, _svg$Element$ElementB13); + + function _class42() { + _classCallCheck(this, _class42); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class42).apply(this, arguments)); + } + + _createClass(_class42, [{ + key: "apply", + value: function apply(ctx, element) { + // render as temp svg + var bb = element.getBoundingBox(); + var x = Math.floor(bb.x1); + var y = Math.floor(bb.y1); + var width = Math.floor(bb.width()); + var height = Math.floor(bb.height()); // temporarily remove filter to avoid recursion + + var filter = element.style('filter').value; + element.style('filter').value = ''; + var px = 0, + py = 0; + this.children.forEach(function (child) { + var efd = child.extraFilterDistance || 0; + px = Math.max(px, efd); + py = Math.max(py, efd); + }); + var c = document.createElement('canvas'); + c.width = width + 2 * px; + c.height = height + 2 * py; + var tempCtx = c.getContext('2d'); + tempCtx.translate(-x + px, -y + py); + element.render(tempCtx); // apply filters + + this.children.forEach(function (child) { + child.apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py); + }); // render on me + + ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py); // reassign filter + + element.style('filter', true).value = filter; + } + }, { + key: "render", + value: function render(ctx) {// NO RENDER + } + }]); + + return _class42; + }(svg.Element.ElementBase); + + svg.Element.feMorphology = + /*#__PURE__*/ + function (_svg$Element$ElementB14) { + _inherits(_class43, _svg$Element$ElementB14); + + function _class43() { + _classCallCheck(this, _class43); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class43).apply(this, arguments)); + } + + _createClass(_class43, [{ + key: "apply", + value: function apply(ctx, x, y, width, height) {// TODO: implement + } + }]); + + return _class43; + }(svg.Element.ElementBase); + + svg.Element.feComposite = + /*#__PURE__*/ + function (_svg$Element$ElementB15) { + _inherits(_class44, _svg$Element$ElementB15); + + function _class44() { + _classCallCheck(this, _class44); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class44).apply(this, arguments)); + } + + _createClass(_class44, [{ + key: "apply", + value: function apply(ctx, x, y, width, height) {// TODO: implement + } + }]); + + return _class44; + }(svg.Element.ElementBase); + /** + * @param {Uint8ClampedArray} img + * @param {Integer} x + * @param {Integer} y + * @param {Float} width + * @param {Float} height + * @param {Integer} rgba + * @returns {Integer} + */ + + + function imGet(img, x, y, width, height, rgba) { + return img[y * width * 4 + x * 4 + rgba]; + } + /** + * @param {Uint8ClampedArray} img + * @param {Integer} x + * @param {Integer} y + * @param {Float} width + * @param {Float} height + * @param {Integer} rgba + * @param {Float} val + * @returns {void} + */ + + + function imSet(img, x, y, width, height, rgba, val) { + img[y * width * 4 + x * 4 + rgba] = val; + } + + svg.Element.feColorMatrix = + /*#__PURE__*/ + function (_svg$Element$ElementB16) { + _inherits(_class45, _svg$Element$ElementB16); + + function _class45(node) { + var _this23; + + _classCallCheck(this, _class45); + + _this23 = _possibleConstructorReturn(this, _getPrototypeOf(_class45).call(this, node)); + var matrix = svg.ToNumberArray(_this23.attribute('values').value); + + switch (_this23.attribute('type').valueOrDefault('matrix')) { + // https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement + case 'saturate': + { + var s = matrix[0]; + matrix = [0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0, 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0, 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]; + break; + } + + case 'hueRotate': + { + var a = matrix[0] * Math.PI / 180.0; + + var c = function c(m1, m2, m3) { + return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; + }; + + matrix = [c(0.213, 0.787, -0.213), c(0.715, -0.715, -0.715), c(0.072, -0.072, 0.928), 0, 0, c(0.213, -0.213, 0.143), c(0.715, 0.285, 0.140), c(0.072, -0.072, -0.283), 0, 0, c(0.213, -0.213, -0.787), c(0.715, -0.715, 0.715), c(0.072, 0.928, 0.072), 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]; + break; + } + + case 'luminanceToAlpha': + matrix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2125, 0.7154, 0.0721, 0, 0, 0, 0, 0, 0, 1]; + break; + } + + _this23.matrix = matrix; + + _this23._m = function (i, v) { + var mi = matrix[i]; + return mi * (mi < 0 ? v - 255 : v); + }; + + return _this23; + } + + _createClass(_class45, [{ + key: "apply", + value: function apply(ctx, x, y, width, height) { + var m = this._m; // assuming x==0 && y==0 for now + + var srcData = ctx.getImageData(0, 0, width, height); + + for (var _y = 0; _y < height; _y++) { + for (var _x = 0; _x < width; _x++) { + var r = imGet(srcData.data, _x, _y, width, height, 0); + var g = imGet(srcData.data, _x, _y, width, height, 1); + var b = imGet(srcData.data, _x, _y, width, height, 2); + var a = imGet(srcData.data, _x, _y, width, height, 3); + imSet(srcData.data, _x, _y, width, height, 0, m(0, r) + m(1, g) + m(2, b) + m(3, a) + m(4, 1)); + imSet(srcData.data, _x, _y, width, height, 1, m(5, r) + m(6, g) + m(7, b) + m(8, a) + m(9, 1)); + imSet(srcData.data, _x, _y, width, height, 2, m(10, r) + m(11, g) + m(12, b) + m(13, a) + m(14, 1)); + imSet(srcData.data, _x, _y, width, height, 3, m(15, r) + m(16, g) + m(17, b) + m(18, a) + m(19, 1)); + } + } + + ctx.clearRect(0, 0, width, height); + ctx.putImageData(srcData, 0, 0); + } + }]); + + return _class45; + }(svg.Element.ElementBase); + + svg.Element.feGaussianBlur = + /*#__PURE__*/ + function (_svg$Element$ElementB17) { + _inherits(_class46, _svg$Element$ElementB17); + + function _class46(node) { + var _this24; + + _classCallCheck(this, _class46); + + _this24 = _possibleConstructorReturn(this, _getPrototypeOf(_class46).call(this, node)); + _this24.blurRadius = Math.floor(_this24.attribute('stdDeviation').numValue()); + _this24.extraFilterDistance = _this24.blurRadius; + return _this24; + } + + _createClass(_class46, [{ + key: "apply", + value: function apply(ctx, x, y, width, height) { + // Todo: This might not be a problem anymore with out `instanceof` fix + // StackBlur requires canvas be on document + ctx.canvas.id = svg.UniqueId(); + ctx.canvas.style.display = 'none'; + document.body.append(ctx.canvas); + processCanvasRGBA(ctx.canvas, x, y, width, height, this.blurRadius); + ctx.canvas.remove(); + } + }]); + + return _class46; + }(svg.Element.ElementBase); // title element, do nothing + + + svg.Element.title = + /*#__PURE__*/ + function (_svg$Element$ElementB18) { + _inherits(_class47, _svg$Element$ElementB18); + + function _class47(node) { + _classCallCheck(this, _class47); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class47).call(this)); + } + + return _class47; + }(svg.Element.ElementBase); // desc element, do nothing + + + svg.Element.desc = + /*#__PURE__*/ + function (_svg$Element$ElementB19) { + _inherits(_class48, _svg$Element$ElementB19); + + function _class48(node) { + _classCallCheck(this, _class48); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class48).call(this)); + } + + return _class48; + }(svg.Element.ElementBase); + + svg.Element.MISSING = + /*#__PURE__*/ + function (_svg$Element$ElementB20) { + _inherits(_class49, _svg$Element$ElementB20); + + function _class49(node) { + var _this25; + + _classCallCheck(this, _class49); + + _this25 = _possibleConstructorReturn(this, _getPrototypeOf(_class49).call(this)); + svg.log('ERROR: Element \'' + node.nodeName + '\' not yet implemented.'); + return _this25; + } + + return _class49; + }(svg.Element.ElementBase); // element factory + + + svg.CreateElement = function (node) { + var className = node.nodeName.replace(/^[^:]+:/, '') // remove namespace + .replace(/-/g, ''); // remove dashes + + var e; + + if (typeof svg.Element[className] !== 'undefined') { + e = new svg.Element[className](node); + } else { + e = new svg.Element.MISSING(node); + } + + e.type = node.nodeName; + return e; + }; // load from url + + + svg.load = function _callee(ctx, url) { + var dom; + return regeneratorRuntime.async(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return regeneratorRuntime.awrap(svg.ajax(url, true)); + + case 2: + dom = _context.sent; + return _context.abrupt("return", svg.loadXml(ctx, dom)); + + case 4: + case "end": + return _context.stop(); + } + } + }); + }; // load from xml + + + svg.loadXml = function (ctx, xml) { + return svg.loadXmlDoc(ctx, svg.parseXml(xml)); + }; + + svg.loadXmlDoc = function (ctx, dom) { + var res; + svg.init(ctx); + + var mapXY = function mapXY(p) { + var e = ctx.canvas; + + while (e) { + p.x -= e.offsetLeft; + p.y -= e.offsetTop; + e = e.offsetParent; + } + + if (window.scrollX) p.x += window.scrollX; + if (window.scrollY) p.y += window.scrollY; + return p; + }; // bind mouse + + + if (svg.opts.ignoreMouse !== true) { + ctx.canvas.addEventListener('click', function (e) { + var args = !isNullish(e) ? [e.clientX, e.clientY] : [event.clientX, event.clientY]; // eslint-disable-line no-restricted-globals + + var _mapXY = mapXY(_construct(svg.Point, args)), + x = _mapXY.x, + y = _mapXY.y; + + svg.Mouse.onclick(x, y); + }); + ctx.canvas.addEventListener('mousemove', function (e) { + var args = !isNullish(e) ? [e.clientX, e.clientY] : [event.clientX, event.clientY]; // eslint-disable-line no-restricted-globals + + var _mapXY2 = mapXY(_construct(svg.Point, args)), + x = _mapXY2.x, + y = _mapXY2.y; + + svg.Mouse.onmousemove(x, y); + }); + } + + var e = svg.CreateElement(dom.documentElement); + e.root = true; // render loop + + var isFirstRender = true; + + var draw = function draw(resolve) { + svg.ViewPort.Clear(); + + if (ctx.canvas.parentNode) { + svg.ViewPort.SetCurrent(ctx.canvas.parentNode.clientWidth, ctx.canvas.parentNode.clientHeight); + } + + if (svg.opts.ignoreDimensions !== true) { + // set canvas size + if (e.style('width').hasValue()) { + ctx.canvas.width = e.style('width').toPixels('x'); + ctx.canvas.style.width = ctx.canvas.width + 'px'; + } + + if (e.style('height').hasValue()) { + ctx.canvas.height = e.style('height').toPixels('y'); + ctx.canvas.style.height = ctx.canvas.height + 'px'; + } + } + + var cWidth = ctx.canvas.clientWidth || ctx.canvas.width; + var cHeight = ctx.canvas.clientHeight || ctx.canvas.height; + + if (svg.opts.ignoreDimensions === true && e.style('width').hasValue() && e.style('height').hasValue()) { + cWidth = e.style('width').toPixels('x'); + cHeight = e.style('height').toPixels('y'); + } + + svg.ViewPort.SetCurrent(cWidth, cHeight); + + if (!isNullish(svg.opts.offsetX)) { + e.attribute('x', true).value = svg.opts.offsetX; + } + + if (!isNullish(svg.opts.offsetY)) { + e.attribute('y', true).value = svg.opts.offsetY; + } + + if (!isNullish(svg.opts.scaleWidth) || !isNullish(svg.opts.scaleHeight)) { + var viewBox = svg.ToNumberArray(e.attribute('viewBox').value); + var xRatio = null, + yRatio = null; + + if (!isNullish(svg.opts.scaleWidth)) { + if (e.attribute('width').hasValue()) { + xRatio = e.attribute('width').toPixels('x') / svg.opts.scaleWidth; + } else if (!isNaN(viewBox[2])) { + xRatio = viewBox[2] / svg.opts.scaleWidth; + } + } + + if (!isNullish(svg.opts.scaleHeight)) { + if (e.attribute('height').hasValue()) { + yRatio = e.attribute('height').toPixels('y') / svg.opts.scaleHeight; + } else if (!isNaN(viewBox[3])) { + yRatio = viewBox[3] / svg.opts.scaleHeight; + } + } + + if (isNullish(xRatio)) { + xRatio = yRatio; + } + + if (isNullish(yRatio)) { + yRatio = xRatio; + } + + e.attribute('width', true).value = svg.opts.scaleWidth; + e.attribute('height', true).value = svg.opts.scaleHeight; + e.attribute('viewBox', true).value = '0 0 ' + cWidth * xRatio + ' ' + cHeight * yRatio; + e.attribute('preserveAspectRatio', true).value = 'none'; + } // clear and render + + + if (svg.opts.ignoreClear !== true) { + ctx.clearRect(0, 0, cWidth, cHeight); + } + + e.render(ctx); + + if (isFirstRender) { + isFirstRender = false; + resolve(dom); + } + }; + + var waitingForImages = true; + svg.intervalID = setInterval(function () { + var needUpdate = false; + + if (waitingForImages && svg.ImagesLoaded()) { + waitingForImages = false; + needUpdate = true; + } // need update from mouse events? + + + if (svg.opts.ignoreMouse !== true) { + needUpdate = needUpdate || svg.Mouse.hasEvents(); + } // need update from animations? + + + if (svg.opts.ignoreAnimation !== true) { + svg.Animations.forEach(function (animation) { + var needAnimationUpdate = animation.update(1000 / svg.FRAMERATE); + needUpdate = needUpdate || needAnimationUpdate; + }); + } // need update from redraw? + + + if (typeof svg.opts.forceRedraw === 'function') { + if (svg.opts.forceRedraw() === true) { + needUpdate = true; + } + } // render if needed + + + if (needUpdate) { + draw(res); + svg.Mouse.runEvents(); // run and clear our events + } + }, 1000 / svg.FRAMERATE); // Todo: Replace with an image loading Promise utility? + + return new Promise(function (resolve, reject) { + // eslint-disable-line promise/avoid-new + if (svg.ImagesLoaded()) { + waitingForImages = false; + draw(resolve); + return; + } + + res = resolve; + }); + }; + + svg.stop = function () { + if (svg.intervalID) { + clearInterval(svg.intervalID); + } + }; + + svg.Mouse = { + events: [], + hasEvents: function hasEvents() { + return this.events.length !== 0; + }, + onclick: function onclick(x, y) { + this.events.push({ + type: 'onclick', + x: x, + y: y, + run: function run(e) { + if (e.onclick) e.onclick(); + } + }); + }, + onmousemove: function onmousemove(x, y) { + this.events.push({ + type: 'onmousemove', + x: x, + y: y, + run: function run(e) { + if (e.onmousemove) e.onmousemove(); + } + }); + }, + eventElements: [], + checkPath: function checkPath(element, ctx) { + var _this26 = this; + + this.events.forEach(function (_ref12, i) { + var x = _ref12.x, + y = _ref12.y; + + if (ctx.isPointInPath && ctx.isPointInPath(x, y)) { + _this26.eventElements[i] = element; + } + }); + }, + checkBoundingBox: function checkBoundingBox(element, bb) { + var _this27 = this; + + this.events.forEach(function (_ref13, i) { + var x = _ref13.x, + y = _ref13.y; + + if (bb.isPointInBox(x, y)) { + _this27.eventElements[i] = element; + } + }); + }, + runEvents: function runEvents() { + var _this28 = this; + + svg.ctx.canvas.style.cursor = ''; + this.events.forEach(function (e, i) { + var element = _this28.eventElements[i]; + + while (element) { + e.run(element); + element = element.parent; + } + }); // done running, clear + + this.events = []; + this.eventElements = []; + } + }; + return svg; + } + + if (typeof CanvasRenderingContext2D !== 'undefined') { + CanvasRenderingContext2D.prototype.drawSvg = function (s, dx, dy, dw, dh) { + canvg(this.canvas, s, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true, + ignoreClear: true, + offsetX: dx, + offsetY: dy, + scaleWidth: dw, + scaleHeight: dh + }); + }; + } + + exports.canvg = canvg; + + return exports; + +}({})); diff --git a/src/image_occlusion_enhanced/svg-edit/dist/dom-polyfill.js b/src/image_occlusion_enhanced/svg-edit/dist/dom-polyfill.js new file mode 100644 index 00000000..05eaefa6 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/dom-polyfill.js @@ -0,0 +1,159 @@ +(function () { + 'use strict'; + + function _typeof(obj) { + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function (obj) { + return typeof obj; + }; + } else { + _typeof = function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); + } + + // From https://github.com/inexorabletash/polyfill/blob/master/dom.js + + /** + * @module DOMPolyfill + */ + + /** + * + * @param {Node} o + * @param {module:DOMPolyfill~ParentNode|module:DOMPolyfill~ChildNode} ps + * @returns {void} + */ + function mixin(o, ps) { + if (!o) return; + Object.keys(ps).forEach(function (p) { + if (p in o || p in o.prototype) { + return; + } + + try { + Object.defineProperty(o.prototype, p, Object.getOwnPropertyDescriptor(ps, p)); + } catch (ex) { + // Throws in IE8; just copy it + o[p] = ps[p]; + } + }); + } + /** + * + * @param {Node[]} nodes + * @returns {Node} + */ + + + function convertNodesIntoANode(nodes) { + nodes = nodes.map(function (node) { + var isNode = node && _typeof(node) === 'object' && 'nodeType' in node; + return isNode ? node : document.createTextNode(node); + }); + + if (nodes.length === 1) { + return nodes[0]; + } + + var node = document.createDocumentFragment(); + nodes.forEach(function (n) { + node.appendChild(n); + }); + return node; + } + + var ParentNode = { + prepend: function prepend() { + for (var _len = arguments.length, nodes = new Array(_len), _key = 0; _key < _len; _key++) { + nodes[_key] = arguments[_key]; + } + + nodes = convertNodesIntoANode(nodes); + this.insertBefore(nodes, this.firstChild); + }, + append: function append() { + for (var _len2 = arguments.length, nodes = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + nodes[_key2] = arguments[_key2]; + } + + nodes = convertNodesIntoANode(nodes); + this.appendChild(nodes); + } + }; + mixin(Document || HTMLDocument, ParentNode); // HTMLDocument for IE8 + + mixin(DocumentFragment, ParentNode); + mixin(Element, ParentNode); // Mixin ChildNode + // https://dom.spec.whatwg.org/#interface-childnode + + var ChildNode = { + before: function before() { + var parent = this.parentNode; + if (!parent) return; + var viablePreviousSibling = this.previousSibling; + + for (var _len3 = arguments.length, nodes = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + nodes[_key3] = arguments[_key3]; + } + + while (nodes.includes(viablePreviousSibling)) { + viablePreviousSibling = viablePreviousSibling.previousSibling; + } + + var node = convertNodesIntoANode(nodes); + parent.insertBefore(node, viablePreviousSibling ? viablePreviousSibling.nextSibling : parent.firstChild); + }, + after: function after() { + var parent = this.parentNode; + if (!parent) return; + var viableNextSibling = this.nextSibling; + + for (var _len4 = arguments.length, nodes = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + nodes[_key4] = arguments[_key4]; + } + + while (nodes.includes(viableNextSibling)) { + viableNextSibling = viableNextSibling.nextSibling; + } + + var node = convertNodesIntoANode(nodes); + parent.insertBefore(node, viableNextSibling); + }, + replaceWith: function replaceWith() { + var parent = this.parentNode; + if (!parent) return; + var viableNextSibling = this.nextSibling; + + for (var _len5 = arguments.length, nodes = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { + nodes[_key5] = arguments[_key5]; + } + + while (nodes.includes(viableNextSibling)) { + viableNextSibling = viableNextSibling.nextSibling; + } + + var node = convertNodesIntoANode(nodes); + + if (this.parentNode === parent) { + parent.replaceChild(node, this); + } else { + parent.insertBefore(node, viableNextSibling); + } + }, + remove: function remove() { + if (!this.parentNode) { + return; + } + + this.parentNode.removeChild(this); // eslint-disable-line unicorn/prefer-node-remove + } + }; + mixin(DocumentType, ChildNode); + mixin(Element, ChildNode); + mixin(CharacterData, ChildNode); + +}()); diff --git a/src/image_occlusion_enhanced/svg-edit/dist/extensions/allowedMimeTypes.php b/src/image_occlusion_enhanced/svg-edit/dist/extensions/allowedMimeTypes.php new file mode 100644 index 00000000..2f99b159 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/extensions/allowedMimeTypes.php @@ -0,0 +1,12 @@ + 'image/svg+xml;charset=UTF-8', + 'png' => 'image/png', + 'jpeg' => 'image/jpeg', + 'bmp' => 'image/bmp', + 'webp' => 'image/webp', + 'pdf' => 'application/pdf' +); + +?> diff --git a/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-arrows.js b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-arrows.js new file mode 100644 index 00000000..c4def664 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-arrows.js @@ -0,0 +1,357 @@ +var svgEditorExtension_arrows = (function () { + 'use strict'; + + /** + * ext-arrows.js + * + * @license MIT + * + * @copyright 2010 Alexis Deveria + * + */ + var extArrows = { + name: 'arrows', + init: function init(S) { + var strings, svgEditor, svgCanvas, addElem, nonce, $, prefix, selElems, arrowprefix, randomizeIds, setArrowNonce, unsetArrowNonce, pathdata, getLinked, showPanel, resetMarker, addMarker, setArrow, colorChanged, contextTools; + return regeneratorRuntime.async(function init$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + colorChanged = function _ref10(elem) { + var color = elem.getAttribute('stroke'); + var mtypes = ['start', 'mid', 'end']; + var defs = svgCanvas.findDefs(); + $.each(mtypes, function (i, type) { + var marker = getLinked(elem, 'marker-' + type); + + if (!marker) { + return; + } + + var curColor = $(marker).children().attr('fill'); + var curD = $(marker).children().attr('d'); + + if (curColor === color) { + return; + } + + var allMarkers = $(defs).find('marker'); + var newMarker = null; // Different color, check if already made + + allMarkers.each(function () { + var attrs = $(this).children().attr(['fill', 'd']); + + if (attrs.fill === color && attrs.d === curD) { + // Found another marker with this color and this path + newMarker = this; // eslint-disable-line consistent-this + } + }); + + if (!newMarker) { + // Create a new marker with this color + var lastId = marker.id; + var dir = lastId.includes('_fw') ? 'fw' : 'bk'; + newMarker = addMarker(dir, type, arrowprefix + dir + allMarkers.length); + $(newMarker).children().attr('fill', color); + } + + $(elem).attr('marker-' + type, 'url(#' + newMarker.id + ')'); // Check if last marker can be removed + + var remove = true; + $(S.svgcontent).find('line, polyline, path, polygon').each(function () { + var element = this; // eslint-disable-line consistent-this + + $.each(mtypes, function (j, mtype) { + if ($(element).attr('marker-' + mtype) === 'url(#' + marker.id + ')') { + remove = false; + return remove; + } + + return undefined; + }); + + if (!remove) { + return false; + } + + return undefined; + }); // Not found, so can safely remove + + if (remove) { + $(marker).remove(); + } + }); + }; + + setArrow = function _ref9() { + resetMarker(); + var type = this.value; + + if (type === 'none') { + return; + } // Set marker on element + + + var dir = 'fw'; + + if (type === 'mid_bk') { + type = 'mid'; + dir = 'bk'; + } else if (type === 'both') { + addMarker('bk', type); + svgCanvas.changeSelectedAttribute('marker-start', 'url(#' + pathdata.bk.id + ')'); + type = 'end'; + dir = 'fw'; + } else if (type === 'start') { + dir = 'bk'; + } + + addMarker(dir, type); + svgCanvas.changeSelectedAttribute('marker-' + type, 'url(#' + pathdata[dir].id + ')'); + svgCanvas.call('changed', selElems); + }; + + addMarker = function _ref8(dir, type, id) { + // TODO: Make marker (or use?) per arrow type, since refX can be different + id = id || arrowprefix + dir; + var data = pathdata[dir]; + + if (type === 'mid') { + data.refx = 5; + } + + var marker = svgCanvas.getElem(id); + + if (!marker) { + marker = addElem({ + element: 'marker', + attr: { + viewBox: '0 0 10 10', + id: id, + refY: 5, + markerUnits: 'strokeWidth', + markerWidth: 5, + markerHeight: 5, + orient: 'auto', + style: 'pointer-events:none' // Currently needed for Opera + + } + }); + var arrow = addElem({ + element: 'path', + attr: { + d: data.d, + fill: '#000000' + } + }); + marker.append(arrow); + svgCanvas.findDefs().append(marker); + } + + marker.setAttribute('refX', data.refx); + return marker; + }; + + resetMarker = function _ref7() { + var el = selElems[0]; + el.removeAttribute('marker-start'); + el.removeAttribute('marker-mid'); + el.removeAttribute('marker-end'); + }; + + showPanel = function _ref6(on) { + $('#arrow_panel').toggle(on); + + if (on) { + var el = selElems[0]; + var end = el.getAttribute('marker-end'); + var start = el.getAttribute('marker-start'); + var mid = el.getAttribute('marker-mid'); + var val; + + if (end && start) { + val = 'both'; + } else if (end) { + val = 'end'; + } else if (start) { + val = 'start'; + } else if (mid) { + val = 'mid'; + + if (mid.includes('bk')) { + val = 'mid_bk'; + } + } + + if (!start && !mid && !end) { + val = 'none'; + } + + $('#arrow_list').val(val); + } + }; + + getLinked = function _ref5(elem, attr) { + var str = elem.getAttribute(attr); + + if (!str) { + return null; + } + + var m = str.match(/\(#(.*)\)/); // const m = str.match(/\(#(?.+)\)/); + // if (!m || !m.groups.id) { + + if (!m || m.length !== 2) { + return null; + } + + return svgCanvas.getElem(m[1]); // return svgCanvas.getElem(m.groups.id); + }; + + unsetArrowNonce = function _ref4(win) { + randomizeIds = false; + arrowprefix = prefix; + pathdata.fw.id = arrowprefix + 'fw'; + pathdata.bk.id = arrowprefix + 'bk'; + }; + + setArrowNonce = function _ref3(win, n) { + randomizeIds = true; + arrowprefix = prefix + n + '_'; + pathdata.fw.id = arrowprefix + 'fw'; + pathdata.bk.id = arrowprefix + 'bk'; + }; + + _context2.next = 10; + return regeneratorRuntime.awrap(S.importLocale()); + + case 10: + strings = _context2.sent; + svgEditor = this; + svgCanvas = svgEditor.canvas; + // {svgcontent} = S, + addElem = svgCanvas.addSVGElementFromJson, nonce = S.nonce, $ = S.$, prefix = 'se_arrow_'; + randomizeIds = S.randomize_ids; + /** + * @param {Window} win + * @param {!(string|Integer)} n + * @returns {void} + */ + + svgCanvas.bind('setnonce', setArrowNonce); + svgCanvas.bind('unsetnonce', unsetArrowNonce); + + if (randomizeIds) { + arrowprefix = prefix + nonce + '_'; + } else { + arrowprefix = prefix; + } + + pathdata = { + fw: { + d: 'm0,0l10,5l-10,5l5,-5l-5,-5z', + refx: 8, + id: arrowprefix + 'fw' + }, + bk: { + d: 'm10,0l-10,5l10,5l-5,-5l5,-5z', + refx: 2, + id: arrowprefix + 'bk' + } + }; + /** + * Gets linked element. + * @param {Element} elem + * @param {string} attr + * @returns {Element} + */ + + contextTools = [{ + type: 'select', + panel: 'arrow_panel', + id: 'arrow_list', + defval: 'none', + events: { + change: setArrow + } + }]; + return _context2.abrupt("return", { + name: strings.name, + context_tools: strings.contextTools.map(function (contextTool, i) { + return Object.assign(contextTools[i], contextTool); + }), + callback: function callback() { + $('#arrow_panel').hide(); // Set ID so it can be translated in locale file + + $('#arrow_list option')[0].id = 'connector_no_arrow'; + }, + addLangData: function addLangData(_ref) { + var lang, importLocale, _ref2, langList; + + return regeneratorRuntime.async(function addLangData$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + lang = _ref.lang, importLocale = _ref.importLocale; + _context.next = 3; + return regeneratorRuntime.awrap(importLocale()); + + case 3: + _ref2 = _context.sent; + langList = _ref2.langList; + return _context.abrupt("return", { + data: langList + }); + + case 6: + case "end": + return _context.stop(); + } + } + }); + }, + selectedChanged: function selectedChanged(opts) { + // Use this to update the current selected elements + selElems = opts.elems; + var markerElems = ['line', 'path', 'polyline', 'polygon']; + var i = selElems.length; + + while (i--) { + var elem = selElems[i]; + + if (elem && markerElems.includes(elem.tagName)) { + if (opts.selectedElement && !opts.multiselected) { + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + }, + elementChanged: function elementChanged(opts) { + var elem = opts.elems[0]; + + if (elem && (elem.getAttribute('marker-start') || elem.getAttribute('marker-mid') || elem.getAttribute('marker-end'))) { + // const start = elem.getAttribute('marker-start'); + // const mid = elem.getAttribute('marker-mid'); + // const end = elem.getAttribute('marker-end'); + // Has marker, so see if it should match color + colorChanged(elem); + } + } + }); + + case 21: + case "end": + return _context2.stop(); + } + } + }, null, this); + } + }; + + return extArrows; + +}()); diff --git a/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-closepath.js b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-closepath.js new file mode 100644 index 00000000..a6643596 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-closepath.js @@ -0,0 +1,129 @@ +var svgEditorExtension_closepath = (function () { + 'use strict'; + + /** + * ext-closepath.js + * + * @license MIT + * + * @copyright 2010 Jeff Schiller + * + */ + // This extension adds a simple button to the contextual panel for paths + // The button toggles whether the path is open or closed + var extClosepath = { + name: 'closepath', + init: function init(_ref) { + var importLocale, $, strings, svgEditor, selElems, updateButton, showPanel, toggleClosed, buttons; + return regeneratorRuntime.async(function init$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + importLocale = _ref.importLocale, $ = _ref.$; + _context.next = 3; + return regeneratorRuntime.awrap(importLocale()); + + case 3: + strings = _context.sent; + svgEditor = this; + + updateButton = function updateButton(path) { + var seglist = path.pathSegList, + closed = seglist.getItem(seglist.numberOfItems - 1).pathSegType === 1, + showbutton = closed ? '#tool_openpath' : '#tool_closepath', + hidebutton = closed ? '#tool_closepath' : '#tool_openpath'; + $(hidebutton).hide(); + $(showbutton).show(); + }; + + showPanel = function showPanel(on) { + $('#closepath_panel').toggle(on); + + if (on) { + var path = selElems[0]; + + if (path) { + updateButton(path); + } + } + }; + + toggleClosed = function toggleClosed() { + var path = selElems[0]; + + if (path) { + var seglist = path.pathSegList, + last = seglist.numberOfItems - 1; // is closed + + if (seglist.getItem(last).pathSegType === 1) { + seglist.removeItem(last); + } else { + seglist.appendItem(path.createSVGPathSegClosePath()); + } + + updateButton(path); + } + }; + + buttons = [{ + id: 'tool_openpath', + icon: svgEditor.curConfig.extIconsPath + 'openpath.png', + type: 'context', + panel: 'closepath_panel', + events: { + click: function click() { + toggleClosed(); + } + } + }, { + id: 'tool_closepath', + icon: svgEditor.curConfig.extIconsPath + 'closepath.png', + type: 'context', + panel: 'closepath_panel', + events: { + click: function click() { + toggleClosed(); + } + } + }]; + return _context.abrupt("return", { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'closepath_icons.svg', + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }), + callback: function callback() { + $('#closepath_panel').hide(); + }, + selectedChanged: function selectedChanged(opts) { + selElems = opts.elems; + var i = selElems.length; + + while (i--) { + var elem = selElems[i]; + + if (elem && elem.tagName === 'path') { + if (opts.selectedElement && !opts.multiselected) { + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + } + }); + + case 10: + case "end": + return _context.stop(); + } + } + }, null, this); + } + }; + + return extClosepath; + +}()); diff --git a/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-connector.js b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-connector.js new file mode 100644 index 00000000..cf43f6a1 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-connector.js @@ -0,0 +1,653 @@ +var svgEditorExtension_connector = (function () { + 'use strict'; + + /* eslint-disable unicorn/no-fn-reference-in-iterator */ + + /** + * ext-connector.js + * + * @license MIT + * + * @copyright 2010 Alexis Deveria + * + */ + var extConnector = { + name: 'connector', + init: function init(S) { + var svgEditor, svgCanvas, getElem, $, svgroot, importLocale, addElem, selManager, connSel, elData, strings, startX, startY, curLine, startElem, endElem, seNs, svgcontent, started, connections, selElems, getBBintersect, getOffset, showPanel, setPoint, updateLine, findConnectors, updateConnectors, init, buttons; + return regeneratorRuntime.async(function init$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + init = function _ref9() { + // Make sure all connectors have data set + $(svgcontent).find('*').each(function () { + var conn = this.getAttributeNS(seNs, 'connector'); + + if (conn) { + this.setAttribute('class', connSel.substr(1)); + var connData = conn.split(' '); + var sbb = svgCanvas.getStrokedBBox([getElem(connData[0])]); + var ebb = svgCanvas.getStrokedBBox([getElem(connData[1])]); + $(this).data('c_start', connData[0]).data('c_end', connData[1]).data('start_bb', sbb).data('end_bb', ebb); + svgCanvas.getEditorNS(true); + } + }); // updateConnectors(); + }; + + updateConnectors = function _ref8(elems) { + // Updates connector lines based on selected elements + // Is not used on mousemove, as it runs getStrokedBBox every time, + // which isn't necessary there. + findConnectors(elems); + + if (connections.length) { + // Update line with element + var i = connections.length; + + while (i--) { + var conn = connections[i]; + var line = conn.connector; + var elem = conn.elem; // const sw = line.getAttribute('stroke-width') * 5; + + var pre = conn.is_start ? 'start' : 'end'; // Update bbox for this element + + var bb = svgCanvas.getStrokedBBox([elem]); + bb.x = conn.start_x; + bb.y = conn.start_y; + elData(line, pre + '_bb', bb); + /* const addOffset = */ + + elData(line, pre + '_off'); + var altPre = conn.is_start ? 'end' : 'start'; // Get center pt of connected element + + var bb2 = elData(line, altPre + '_bb'); + var srcX = bb2.x + bb2.width / 2; + var srcY = bb2.y + bb2.height / 2; // Set point of element being moved + + var pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); + setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); // Set point of connected element + + var pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); + setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); // Update points attribute manually for webkit + + if (navigator.userAgent.includes('AppleWebKit')) { + var pts = line.points; + var len = pts.numberOfItems; + var ptArr = []; + + for (var j = 0; j < len; j++) { + pt = pts.getItem(j); + ptArr[j] = pt.x + ',' + pt.y; + } + + line.setAttribute('points', ptArr.join(' ')); + } + } + } + }; + + findConnectors = function _ref7() { + var elems = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : selElems; + var connectors = $(svgcontent).find(connSel); + connections = []; // Loop through connectors to see if one is connected to the element + + connectors.each(function () { + var addThis; + /** + * + * @returns {void} + */ + + function add() { + if (elems.includes(this)) { + // Pretend this element is selected + addThis = true; + } + } // Grab the ends + + + var parts = []; + ['start', 'end'].forEach(function (pos, i) { + var key = 'c_' + pos; + var part = elData(this, key); + + if (part === null || part === undefined) { + // Does this ever return nullish values? + part = document.getElementById(this.attributes['se:connector'].value.split(' ')[i]); + elData(this, 'c_' + pos, part.id); + elData(this, pos + '_bb', svgCanvas.getStrokedBBox([part])); + } else part = document.getElementById(part); + + parts.push(part); + }, this); + + for (var i = 0; i < 2; i++) { + var cElem = parts[i]; + addThis = false; // The connected element might be part of a selected group + + $(cElem).parents().each(add); + + if (!cElem || !cElem.parentNode) { + $(this).remove(); + continue; + } + + if (elems.includes(cElem) || addThis) { + var bb = svgCanvas.getStrokedBBox([cElem]); + connections.push({ + elem: cElem, + connector: this, + is_start: i === 0, + start_x: bb.x, + start_y: bb.y + }); + } + } + }); + }; + + updateLine = function _ref6(diffX, diffY) { + // Update line with element + var i = connections.length; + + while (i--) { + var conn = connections[i]; + var line = conn.connector; // const {elem} = conn; + + var pre = conn.is_start ? 'start' : 'end'; // const sw = line.getAttribute('stroke-width') * 5; + // Update bbox for this element + + var bb = elData(line, pre + '_bb'); + bb.x = conn.start_x + diffX; + bb.y = conn.start_y + diffY; + elData(line, pre + '_bb', bb); + var altPre = conn.is_start ? 'end' : 'start'; // Get center pt of connected element + + var bb2 = elData(line, altPre + '_bb'); + var srcX = bb2.x + bb2.width / 2; + var srcY = bb2.y + bb2.height / 2; // Set point of element being moved + + var pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0 + + setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); // Set point of connected element + + var pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); + setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); + } + }; + + setPoint = function _ref5(elem, pos, x, y, setMid) { + var pts = elem.points; + var pt = svgroot.createSVGPoint(); + pt.x = x; + pt.y = y; + + if (pos === 'end') { + pos = pts.numberOfItems - 1; + } // TODO: Test for this on init, then use alt only if needed + + + try { + pts.replaceItem(pt, pos); + } catch (err) { + // Should only occur in FF which formats points attr as "n,n n,n", so just split + var ptArr = elem.getAttribute('points').split(' '); + + for (var i = 0; i < ptArr.length; i++) { + if (i === pos) { + ptArr[i] = x + ',' + y; + } + } + + elem.setAttribute('points', ptArr.join(' ')); + } + + if (setMid) { + // Add center point + var ptStart = pts.getItem(0); + var ptEnd = pts.getItem(pts.numberOfItems - 1); + setPoint(elem, 1, (ptEnd.x + ptStart.x) / 2, (ptEnd.y + ptStart.y) / 2); + } + }; + + showPanel = function _ref4(on) { + var connRules = $('#connector_rules'); + + if (!connRules.length) { + connRules = $('').appendTo('head'); + } + + connRules.text(!on ? '' : '#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }'); + $('#connector_panel').toggle(on); + }; + + getOffset = function _ref3(side, line) { + var giveOffset = line.getAttribute('marker-' + side); // const giveOffset = $(line).data(side+'_off'); + // TODO: Make this number (5) be based on marker width/height + + var size = line.getAttribute('stroke-width') * 5; + return giveOffset ? size : 0; + }; + + getBBintersect = function _ref2(x, y, bb, offset) { + if (offset) { + offset -= 0; + bb = $.extend({}, bb); + bb.width += offset; + bb.height += offset; + bb.x -= offset / 2; + bb.y -= offset / 2; + } + + var midX = bb.x + bb.width / 2; + var midY = bb.y + bb.height / 2; + var lenX = x - midX; + var lenY = y - midY; + var slope = Math.abs(lenY / lenX); + var ratio; + + if (slope < bb.height / bb.width) { + ratio = bb.width / 2 / Math.abs(lenX); + } else { + ratio = lenY ? bb.height / 2 / Math.abs(lenY) : 0; + } + + return { + x: midX + lenX * ratio, + y: midY + lenY * ratio + }; + }; + + svgEditor = this; + svgCanvas = svgEditor.canvas; + getElem = svgCanvas.getElem; + $ = S.$, svgroot = S.svgroot, importLocale = S.importLocale, addElem = svgCanvas.addSVGElementFromJson, selManager = S.selectorManager, connSel = '.se_connector', elData = $.data; + _context.next = 14; + return regeneratorRuntime.awrap(importLocale()); + + case 14: + strings = _context.sent; + svgcontent = S.svgcontent, started = false, connections = [], selElems = []; + /** + * + * @param {Float} x + * @param {Float} y + * @param {module:utilities.BBoxObject} bb + * @param {Float} offset + * @returns {module:math.XYObject} + */ + + // Do once + (function () { + var gse = svgCanvas.groupSelectedElements; + + svgCanvas.groupSelectedElements = function () { + svgCanvas.removeFromSelection($(connSel).toArray()); + + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return gse.apply(this, args); + }; + + var mse = svgCanvas.moveSelectedElements; + + svgCanvas.moveSelectedElements = function () { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + var cmd = mse.apply(this, args); + updateConnectors(); + return cmd; + }; + + seNs = svgCanvas.getEditorNS(); + })(); + /** + * Do on reset. + * @returns {void} + */ + + + // $(svgroot).parent().mousemove(function (e) { + // // if (started + // // || svgCanvas.getMode() !== 'connector' + // // || e.target.parentNode.parentNode !== svgcontent) return; + // + // console.log('y') + // // if (e.target.parentNode.parentNode === svgcontent) { + // // + // // } + // }); + buttons = [{ + id: 'mode_connect', + type: 'mode', + icon: svgEditor.curConfig.imgPath + 'cut.png', + includeWith: { + button: '#tool_line', + isDefault: false, + position: 1 + }, + events: { + click: function click() { + svgCanvas.setMode('connector'); + } + } + }]; + return _context.abrupt("return", { + name: strings.name, + svgicons: svgEditor.curConfig.imgPath + 'conn.svg', + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }), + + /* async */ + addLangData: function addLangData(_ref) { + var lang = _ref.lang; + // , importLocale: importLoc + return { + data: strings.langList + }; + }, + mouseDown: function mouseDown(opts) { + var e = opts.event; + startX = opts.start_x; + startY = opts.start_y; + var mode = svgCanvas.getMode(); + var initStroke = svgEditor.curConfig.initStroke; + + if (mode === 'connector') { + if (started) { + return undefined; + } + + var mouseTarget = e.target; + var parents = $(mouseTarget).parents(); + + if ($.inArray(svgcontent, parents) !== -1) { + // Connectable element + // If child of foreignObject, use parent + var fo = $(mouseTarget).closest('foreignObject'); + startElem = fo.length ? fo[0] : mouseTarget; // Get center of source element + + var bb = svgCanvas.getStrokedBBox([startElem]); + var x = bb.x + bb.width / 2; + var y = bb.y + bb.height / 2; + started = true; + curLine = addElem({ + element: 'polyline', + attr: { + id: svgCanvas.getNextId(), + points: x + ',' + y + ' ' + x + ',' + y + ' ' + startX + ',' + startY, + stroke: '#' + initStroke.color, + 'stroke-width': !startElem.stroke_width || startElem.stroke_width === 0 ? initStroke.width : startElem.stroke_width, + fill: 'none', + opacity: initStroke.opacity, + style: 'pointer-events:none' + } + }); + elData(curLine, 'start_bb', bb); + } + + return { + started: true + }; + } + + if (mode === 'select') { + findConnectors(); + } + + return undefined; + }, + mouseMove: function mouseMove(opts) { + var zoom = svgCanvas.getZoom(); // const e = opts.event; + + var x = opts.mouse_x / zoom; + var y = opts.mouse_y / zoom; + var diffX = x - startX, + diffY = y - startY; + var mode = svgCanvas.getMode(); + + if (mode === 'connector' && started) { + // const sw = curLine.getAttribute('stroke-width') * 3; + // Set start point (adjusts based on bb) + var pt = getBBintersect(x, y, elData(curLine, 'start_bb'), getOffset('start', curLine)); + startX = pt.x; + startY = pt.y; + setPoint(curLine, 0, pt.x, pt.y, true); // Set end point + + setPoint(curLine, 'end', x, y, true); + } else if (mode === 'select') { + var slen = selElems.length; + + while (slen--) { + var elem = selElems[slen]; // Look for selected connector elements + + if (elem && elData(elem, 'c_start')) { + // Remove the "translate" transform given to move + svgCanvas.removeFromSelection([elem]); + svgCanvas.getTransformList(elem).clear(); + } + } + + if (connections.length) { + updateLine(diffX, diffY); + } + } + }, + mouseUp: function mouseUp(opts) { + // const zoom = svgCanvas.getZoom(); + var e = opts.event; // , x = opts.mouse_x / zoom, + // , y = opts.mouse_y / zoom, + + var mouseTarget = e.target; + + if (svgCanvas.getMode() !== 'connector') { + return undefined; + } + + var fo = $(mouseTarget).closest('foreignObject'); + + if (fo.length) { + mouseTarget = fo[0]; + } + + var parents = $(mouseTarget).parents(); + + if (mouseTarget === startElem) { + // Start line through click + started = true; + return { + keep: true, + element: null, + started: started + }; + } + + if ($.inArray(svgcontent, parents) === -1) { + // Not a valid target element, so remove line + $(curLine).remove(); + started = false; + return { + keep: false, + element: null, + started: started + }; + } // Valid end element + + + endElem = mouseTarget; + var startId = startElem.id, + endId = endElem.id; + var connStr = startId + ' ' + endId; + var altStr = endId + ' ' + startId; // Don't create connector if one already exists + + var dupe = $(svgcontent).find(connSel).filter(function () { + var conn = this.getAttributeNS(seNs, 'connector'); + + if (conn === connStr || conn === altStr) { + return true; + } + + return false; + }); + + if (dupe.length) { + $(curLine).remove(); + return { + keep: false, + element: null, + started: false + }; + } + + var bb = svgCanvas.getStrokedBBox([endElem]); + var pt = getBBintersect(startX, startY, bb, getOffset('start', curLine)); + setPoint(curLine, 'end', pt.x, pt.y, true); + $(curLine).data('c_start', startId).data('c_end', endId).data('end_bb', bb); + seNs = svgCanvas.getEditorNS(true); + curLine.setAttributeNS(seNs, 'se:connector', connStr); + curLine.setAttribute('class', connSel.substr(1)); + curLine.setAttribute('opacity', 1); + svgCanvas.addToSelection([curLine]); + svgCanvas.moveToBottomSelectedElement(); + selManager.requestSelector(curLine).showGrips(false); + started = false; + return { + keep: true, + element: curLine, + started: started + }; + }, + selectedChanged: function selectedChanged(opts) { + // TODO: Find better way to skip operations if no connectors are in use + if (!$(svgcontent).find(connSel).length) { + return; + } + + if (svgCanvas.getMode() === 'connector') { + svgCanvas.setMode('select'); + } // Use this to update the current selected elements + + + selElems = opts.elems; + var i = selElems.length; + + while (i--) { + var elem = selElems[i]; + + if (elem && elData(elem, 'c_start')) { + selManager.requestSelector(elem).showGrips(false); + + if (opts.selectedElement && !opts.multiselected) { + // TODO: Set up context tools and hide most regular line tools + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + + updateConnectors(); + }, + elementChanged: function elementChanged(opts) { + var elem = opts.elems[0]; + + if (elem && elem.tagName === 'svg' && elem.id === 'svgcontent') { + // Update svgcontent (can change on import) + svgcontent = elem; + init(); + } // Has marker, so change offset + + + if (elem && (elem.getAttribute('marker-start') || elem.getAttribute('marker-mid') || elem.getAttribute('marker-end'))) { + var start = elem.getAttribute('marker-start'); + var mid = elem.getAttribute('marker-mid'); + var end = elem.getAttribute('marker-end'); + curLine = elem; + $(elem).data('start_off', Boolean(start)).data('end_off', Boolean(end)); + + if (elem.tagName === 'line' && mid) { + // Convert to polyline to accept mid-arrow + var x1 = Number(elem.getAttribute('x1')); + var x2 = Number(elem.getAttribute('x2')); + var y1 = Number(elem.getAttribute('y1')); + var y2 = Number(elem.getAttribute('y2')); + var _elem = elem, + id = _elem.id; + var midPt = ' ' + (x1 + x2) / 2 + ',' + (y1 + y2) / 2 + ' '; + var pline = addElem({ + element: 'polyline', + attr: { + points: x1 + ',' + y1 + midPt + x2 + ',' + y2, + stroke: elem.getAttribute('stroke'), + 'stroke-width': elem.getAttribute('stroke-width'), + 'marker-mid': mid, + fill: 'none', + opacity: elem.getAttribute('opacity') || 1 + } + }); + $(elem).after(pline).remove(); + svgCanvas.clearSelection(); + pline.id = id; + svgCanvas.addToSelection([pline]); + elem = pline; + } + } // Update line if it's a connector + + + if (elem.getAttribute('class') === connSel.substr(1)) { + var _start = getElem(elData(elem, 'c_start')); + + updateConnectors([_start]); + } else { + updateConnectors(); + } + }, + IDsUpdated: function IDsUpdated(input) { + var remove = []; + input.elems.forEach(function (elem) { + if ('se:connector' in elem.attr) { + elem.attr['se:connector'] = elem.attr['se:connector'].split(' ').map(function (oldID) { + return input.changes[oldID]; + }).join(' '); // Check validity - the field would be something like 'svg_21 svg_22', but + // if one end is missing, it would be 'svg_21' and therefore fail this test + + if (!/. ./.test(elem.attr['se:connector'])) { + remove.push(elem.attr.id); + } + } + }); + return { + remove: remove + }; + }, + toolButtonStateUpdate: function toolButtonStateUpdate(opts) { + if (opts.nostroke) { + if ($('#mode_connect').hasClass('tool_button_current')) { + svgEditor.clickSelect(); + } + } + + $('#mode_connect').toggleClass('disabled', opts.nostroke); + } + }); + + case 19: + case "end": + return _context.stop(); + } + } + }, null, this); + } + }; + + return extConnector; + +}()); diff --git a/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-eyedropper.js b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-eyedropper.js new file mode 100644 index 00000000..c00b7c69 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-eyedropper.js @@ -0,0 +1,162 @@ +var svgEditorExtension_eyedropper = (function () { + 'use strict'; + + /** + * ext-eyedropper.js + * + * @license MIT + * + * @copyright 2010 Jeff Schiller + * + */ + var extEyedropper = { + name: 'eyedropper', + init: function init(S) { + var strings, svgEditor, $, ChangeElementCommand, svgCanvas, addToHistory, currentStyle, getStyle, buttons; + return regeneratorRuntime.async(function init$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + getStyle = function _ref(opts) { + // if we are in eyedropper mode, we don't want to disable the eye-dropper tool + var mode = svgCanvas.getMode(); + + if (mode === 'eyedropper') { + return; + } + + var tool = $('#tool_eyedropper'); // enable-eye-dropper if one element is selected + + var elem = null; + + if (!opts.multiselected && opts.elems[0] && !['svg', 'g', 'use'].includes(opts.elems[0].nodeName)) { + elem = opts.elems[0]; + tool.removeClass('disabled'); // grab the current style + + currentStyle.fillPaint = elem.getAttribute('fill') || 'black'; + currentStyle.fillOpacity = elem.getAttribute('fill-opacity') || 1.0; + currentStyle.strokePaint = elem.getAttribute('stroke'); + currentStyle.strokeOpacity = elem.getAttribute('stroke-opacity') || 1.0; + currentStyle.strokeWidth = elem.getAttribute('stroke-width'); + currentStyle.strokeDashArray = elem.getAttribute('stroke-dasharray'); + currentStyle.strokeLinecap = elem.getAttribute('stroke-linecap'); + currentStyle.strokeLinejoin = elem.getAttribute('stroke-linejoin'); + currentStyle.opacity = elem.getAttribute('opacity') || 1.0; // disable eye-dropper tool + } else { + tool.addClass('disabled'); + } + }; + + _context.next = 3; + return regeneratorRuntime.awrap(S.importLocale()); + + case 3: + strings = _context.sent; + svgEditor = this; + $ = S.$, ChangeElementCommand = S.ChangeElementCommand, svgCanvas = svgEditor.canvas, addToHistory = function addToHistory(cmd) { + svgCanvas.undoMgr.addCommandToHistory(cmd); + }, currentStyle = { + fillPaint: 'red', + fillOpacity: 1.0, + strokePaint: 'black', + strokeOpacity: 1.0, + strokeWidth: 5, + strokeDashArray: null, + opacity: 1.0, + strokeLinecap: 'butt', + strokeLinejoin: 'miter' + }; + /** + * + * @param {module:svgcanvas.SvgCanvas#event:ext_selectedChanged|module:svgcanvas.SvgCanvas#event:ext_elementChanged} opts + * @returns {void} + */ + + buttons = [{ + id: 'tool_eyedropper', + icon: svgEditor.curConfig.extIconsPath + 'eyedropper.png', + type: 'mode', + events: { + click: function click() { + svgCanvas.setMode('eyedropper'); + } + } + }]; + return _context.abrupt("return", { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'eyedropper-icon.xml', + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }), + // if we have selected an element, grab its paint and enable the eye dropper button + selectedChanged: getStyle, + elementChanged: getStyle, + mouseDown: function mouseDown(opts) { + var mode = svgCanvas.getMode(); + + if (mode === 'eyedropper') { + var e = opts.event; + var target = e.target; + + if (!['svg', 'g', 'use'].includes(target.nodeName)) { + var changes = {}; + + var change = function change(elem, attrname, newvalue) { + changes[attrname] = elem.getAttribute(attrname); + elem.setAttribute(attrname, newvalue); + }; + + if (currentStyle.fillPaint) { + change(target, 'fill', currentStyle.fillPaint); + } + + if (currentStyle.fillOpacity) { + change(target, 'fill-opacity', currentStyle.fillOpacity); + } + + if (currentStyle.strokePaint) { + change(target, 'stroke', currentStyle.strokePaint); + } + + if (currentStyle.strokeOpacity) { + change(target, 'stroke-opacity', currentStyle.strokeOpacity); + } + + if (currentStyle.strokeWidth) { + change(target, 'stroke-width', currentStyle.strokeWidth); + } + + if (currentStyle.strokeDashArray) { + change(target, 'stroke-dasharray', currentStyle.strokeDashArray); + } + + if (currentStyle.opacity) { + change(target, 'opacity', currentStyle.opacity); + } + + if (currentStyle.strokeLinecap) { + change(target, 'stroke-linecap', currentStyle.strokeLinecap); + } + + if (currentStyle.strokeLinejoin) { + change(target, 'stroke-linejoin', currentStyle.strokeLinejoin); + } + + addToHistory(new ChangeElementCommand(target, changes)); + } + } + } + }); + + case 8: + case "end": + return _context.stop(); + } + } + }, null, this); + } + }; + + return extEyedropper; + +}()); diff --git a/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-foreignobject.js b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-foreignobject.js new file mode 100644 index 00000000..b327b8b9 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-foreignobject.js @@ -0,0 +1,323 @@ +var svgEditorExtension_foreignobject = (function () { + 'use strict'; + + /** + * ext-foreignobject.js + * + * @license Apache-2.0 + * + * @copyright 2010 Jacques Distler, 2010 Alexis Deveria + * + */ + var extForeignobject = { + name: 'foreignobject', + init: function init(S) { + var svgEditor, $, text2xml, NS, importLocale, svgCanvas, svgdoc, strings, properlySourceSizeTextArea, showPanel, toggleSourceButtons, selElems, started, newFO, editingforeign, setForeignString, showForeignEditor, setAttr, buttons, contextTools; + return regeneratorRuntime.async(function init$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + setAttr = function _ref5(attr, val) { + svgCanvas.changeSelectedAttribute(attr, val); + svgCanvas.call('changed', selElems); + }; + + showForeignEditor = function _ref4() { + var elt = selElems[0]; + + if (!elt || editingforeign) { + return; + } + + editingforeign = true; + toggleSourceButtons(true); + elt.removeAttribute('fill'); + var str = svgCanvas.svgToString(elt, 0); + $('#svg_source_textarea').val(str); + $('#svg_source_editor').fadeIn(); + properlySourceSizeTextArea(); + $('#svg_source_textarea').focus(); + }; + + setForeignString = function _ref3(xmlString) { + var elt = selElems[0]; // The parent `Element` to append to + + try { + // convert string into XML document + var newDoc = text2xml('' + xmlString + ''); // run it through our sanitizer to remove anything we do not support + + svgCanvas.sanitizeSvg(newDoc.documentElement); + elt.replaceWith(svgdoc.importNode(newDoc.documentElement.firstChild, true)); + svgCanvas.call('changed', [elt]); + svgCanvas.clearSelection(); + } catch (e) { + // Todo: Surface error to user + console.log(e); // eslint-disable-line no-console + + return false; + } + + return true; + }; + + toggleSourceButtons = function _ref2(on) { + $('#tool_source_save, #tool_source_cancel').toggle(!on); + $('#foreign_save, #foreign_cancel').toggle(on); + }; + + showPanel = function _ref(on) { + var fcRules = $('#fc_rules'); + + if (!fcRules.length) { + fcRules = $('').appendTo('head'); + } + + fcRules.text(!on ? '' : ' #tool_topath { display: none !important; }'); + $('#foreignObject_panel').toggle(on); + }; + + svgEditor = this; + $ = S.$, text2xml = S.text2xml, NS = S.NS, importLocale = S.importLocale; + svgCanvas = svgEditor.canvas; + svgdoc = S.svgroot.parentNode.ownerDocument; + _context2.next = 11; + return regeneratorRuntime.awrap(importLocale()); + + case 11: + strings = _context2.sent; + + properlySourceSizeTextArea = function properlySourceSizeTextArea() { + // TODO: remove magic numbers here and get values from CSS + var height = $('#svg_source_container').height() - 80; + $('#svg_source_textarea').css('height', height); + }; + /** + * @param {boolean} on + * @returns {void} + */ + + + editingforeign = false; + /** + * This function sets the content of element elt to the input XML. + * @param {string} xmlString - The XML text + * @returns {boolean} This function returns false if the set was unsuccessful, true otherwise. + */ + + buttons = [{ + id: 'tool_foreign', + icon: svgEditor.curConfig.extIconsPath + 'foreignobject-tool.png', + type: 'mode', + events: { + click: function click() { + svgCanvas.setMode('foreign'); + } + } + }, { + id: 'edit_foreign', + icon: svgEditor.curConfig.extIconsPath + 'foreignobject-edit.png', + type: 'context', + panel: 'foreignObject_panel', + events: { + click: function click() { + showForeignEditor(); + } + } + }]; + contextTools = [{ + type: 'input', + panel: 'foreignObject_panel', + id: 'foreign_width', + size: 3, + events: { + change: function change() { + setAttr('width', this.value); + } + } + }, { + type: 'input', + panel: 'foreignObject_panel', + id: 'foreign_height', + events: { + change: function change() { + setAttr('height', this.value); + } + } + }, { + type: 'input', + panel: 'foreignObject_panel', + id: 'foreign_font_size', + size: 2, + defval: 16, + events: { + change: function change() { + setAttr('font-size', this.value); + } + } + }]; + return _context2.abrupt("return", { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'foreignobject-icons.xml', + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }), + context_tools: strings.contextTools.map(function (contextTool, i) { + return Object.assign(contextTools[i], contextTool); + }), + callback: function callback() { + $('#foreignObject_panel').hide(); + + var endChanges = function endChanges() { + $('#svg_source_editor').hide(); + editingforeign = false; + $('#svg_source_textarea').blur(); + toggleSourceButtons(false); + }; // TODO: Needs to be done after orig icon loads + + + setTimeout(function () { + // Create source save/cancel buttons + + /* const save = */ + $('#tool_source_save').clone().hide().attr('id', 'foreign_save').unbind().appendTo('#tool_source_back').click(function _callee() { + var ok; + return regeneratorRuntime.async(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + if (editingforeign) { + _context.next = 2; + break; + } + + return _context.abrupt("return"); + + case 2: + if (setForeignString($('#svg_source_textarea').val())) { + _context.next = 11; + break; + } + + _context.next = 5; + return regeneratorRuntime.awrap($.confirm('Errors found. Revert to original?')); + + case 5: + ok = _context.sent; + + if (ok) { + _context.next = 8; + break; + } + + return _context.abrupt("return"); + + case 8: + endChanges(); + _context.next = 12; + break; + + case 11: + endChanges(); + + case 12: + case "end": + return _context.stop(); + } + } + }); + }); + /* const cancel = */ + + $('#tool_source_cancel').clone().hide().attr('id', 'foreign_cancel').unbind().appendTo('#tool_source_back').click(function () { + endChanges(); + }); + }, 3000); + }, + mouseDown: function mouseDown(opts) { + // const e = opts.event; + if (svgCanvas.getMode() !== 'foreign') { + return undefined; + } + + started = true; + newFO = svgCanvas.addSVGElementFromJson({ + element: 'foreignObject', + attr: { + x: opts.start_x, + y: opts.start_y, + id: svgCanvas.getNextId(), + 'font-size': 16, + // cur_text.font_size, + width: '48', + height: '20', + style: 'pointer-events:inherit' + } + }); + var m = svgdoc.createElementNS(NS.MATH, 'math'); + m.setAttributeNS(NS.XMLNS, 'xmlns', NS.MATH); + m.setAttribute('display', 'inline'); + var mi = svgdoc.createElementNS(NS.MATH, 'mi'); + mi.setAttribute('mathvariant', 'normal'); + mi.textContent = "\u03A6"; + var mo = svgdoc.createElementNS(NS.MATH, 'mo'); + mo.textContent = "\u222A"; + var mi2 = svgdoc.createElementNS(NS.MATH, 'mi'); + mi2.textContent = "\u2133"; + m.append(mi, mo, mi2); + newFO.append(m); + return { + started: true + }; + }, + mouseUp: function mouseUp(opts) { + // const e = opts.event; + if (svgCanvas.getMode() !== 'foreign' || !started) { + return undefined; + } + + var attrs = $(newFO).attr(['width', 'height']); + var keep = attrs.width !== '0' || attrs.height !== '0'; + svgCanvas.addToSelection([newFO], true); + return { + keep: keep, + element: newFO + }; + }, + selectedChanged: function selectedChanged(opts) { + // Use this to update the current selected elements + selElems = opts.elems; + var i = selElems.length; + + while (i--) { + var elem = selElems[i]; + + if (elem && elem.tagName === 'foreignObject') { + if (opts.selectedElement && !opts.multiselected) { + $('#foreign_font_size').val(elem.getAttribute('font-size')); + $('#foreign_width').val(elem.getAttribute('width')); + $('#foreign_height').val(elem.getAttribute('height')); + showPanel(true); + } else { + showPanel(false); + } + } else { + showPanel(false); + } + } + }, + elementChanged: function elementChanged(opts) {// const elem = opts.elems[0]; + } + }); + + case 17: + case "end": + return _context2.stop(); + } + } + }, null, this); + } + }; + + return extForeignobject; + +}()); diff --git a/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-grid.js b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-grid.js new file mode 100644 index 00000000..0c71125e --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-grid.js @@ -0,0 +1,182 @@ +var svgEditorExtension_grid = (function () { + 'use strict'; + + /** + * ext-grid.js + * + * @license Apache-2.0 + * + * @copyright 2010 Redou Mine, 2010 Alexis Deveria + * + */ + var extGrid = { + name: 'grid', + init: function init(_ref) { + var $, NS, getTypeMap, importLocale, strings, svgEditor, svgCanvas, svgdoc, assignAttributes, hcanvas, canvBG, units, intervals, showGrid, canvasGrid, gridDefs, gridPattern, gridimg, gridBox, updateGrid, gridUpdate, buttons; + return regeneratorRuntime.async(function init$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + gridUpdate = function _ref3() { + if (showGrid) { + updateGrid(svgCanvas.getZoom()); + } + + $('#canvasGrid').toggle(showGrid); + $('#view_grid').toggleClass('push_button_pressed tool_button'); + }; + + updateGrid = function _ref2(zoom) { + // TODO: Try this with elements, then compare performance difference + var unit = units[svgEditor.curConfig.baseUnit]; // 1 = 1px + + var uMulti = unit * zoom; // Calculate the main number interval + + var rawM = 100 / uMulti; + var multi = 1; + intervals.some(function (num) { + multi = num; + return rawM <= num; + }); + var bigInt = multi * uMulti; // Set the canvas size to the width of the container + + hcanvas.width = bigInt; + hcanvas.height = bigInt; + var ctx = hcanvas.getContext('2d'); + var curD = 0.5; + var part = bigInt / 10; + ctx.globalAlpha = 0.2; + ctx.strokeStyle = svgEditor.curConfig.gridColor; + + for (var i = 1; i < 10; i++) { + var subD = Math.round(part * i) + 0.5; // const lineNum = (i % 2)?12:10; + + var lineNum = 0; + ctx.moveTo(subD, bigInt); + ctx.lineTo(subD, lineNum); + ctx.moveTo(bigInt, subD); + ctx.lineTo(lineNum, subD); + } + + ctx.stroke(); + ctx.beginPath(); + ctx.globalAlpha = 0.5; + ctx.moveTo(curD, bigInt); + ctx.lineTo(curD, 0); + ctx.moveTo(bigInt, curD); + ctx.lineTo(0, curD); + ctx.stroke(); + var datauri = hcanvas.toDataURL('image/png'); + gridimg.setAttribute('width', bigInt); + gridimg.setAttribute('height', bigInt); + gridimg.parentNode.setAttribute('width', bigInt); + gridimg.parentNode.setAttribute('height', bigInt); + svgCanvas.setHref(gridimg, datauri); + }; + + $ = _ref.$, NS = _ref.NS, getTypeMap = _ref.getTypeMap, importLocale = _ref.importLocale; + _context.next = 5; + return regeneratorRuntime.awrap(importLocale()); + + case 5: + strings = _context.sent; + svgEditor = this; + svgCanvas = svgEditor.canvas; + svgdoc = document.getElementById('svgcanvas').ownerDocument, assignAttributes = svgCanvas.assignAttributes, hcanvas = document.createElement('canvas'), canvBG = $('#canvasBackground'), units = getTypeMap(), intervals = [0.01, 0.1, 1, 10, 100, 1000]; + showGrid = svgEditor.curConfig.showGrid || false; + $(hcanvas).hide().appendTo('body'); + canvasGrid = svgdoc.createElementNS(NS.SVG, 'svg'); + assignAttributes(canvasGrid, { + id: 'canvasGrid', + width: '100%', + height: '100%', + x: 0, + y: 0, + overflow: 'visible', + display: 'none' + }); + canvBG.append(canvasGrid); + gridDefs = svgdoc.createElementNS(NS.SVG, 'defs'); // grid-pattern + + gridPattern = svgdoc.createElementNS(NS.SVG, 'pattern'); + assignAttributes(gridPattern, { + id: 'gridpattern', + patternUnits: 'userSpaceOnUse', + x: 0, + // -(value.strokeWidth / 2), // position for strokewidth + y: 0, + // -(value.strokeWidth / 2), // position for strokewidth + width: 100, + height: 100 + }); + gridimg = svgdoc.createElementNS(NS.SVG, 'image'); + assignAttributes(gridimg, { + x: 0, + y: 0, + width: 100, + height: 100 + }); + gridPattern.append(gridimg); + gridDefs.append(gridPattern); + $('#canvasGrid').append(gridDefs); // grid-box + + gridBox = svgdoc.createElementNS(NS.SVG, 'rect'); + assignAttributes(gridBox, { + width: '100%', + height: '100%', + x: 0, + y: 0, + 'stroke-width': 0, + stroke: 'none', + fill: 'url(#gridpattern)', + style: 'pointer-events: none; display:visible;' + }); + $('#canvasGrid').append(gridBox); + /** + * + * @param {Float} zoom + * @returns {void} + */ + + buttons = [{ + id: 'view_grid', + icon: svgEditor.curConfig.extIconsPath + 'grid.png', + type: 'context', + panel: 'editor_panel', + events: { + click: function click() { + svgEditor.curConfig.showGrid = showGrid = !showGrid; + gridUpdate(); + } + } + }]; + return _context.abrupt("return", { + name: strings.name, + svgicons: svgEditor.curConfig.extIconsPath + 'grid-icon.xml', + zoomChanged: function zoomChanged(zoom) { + if (showGrid) { + updateGrid(zoom); + } + }, + callback: function callback() { + if (showGrid) { + gridUpdate(); + } + }, + buttons: strings.buttons.map(function (button, i) { + return Object.assign(buttons[i], button); + }) + }); + + case 27: + case "end": + return _context.stop(); + } + } + }, null, this); + } + }; + + return extGrid; + +}()); diff --git a/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-helloworld.js b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-helloworld.js new file mode 100644 index 00000000..f29718b9 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-helloworld.js @@ -0,0 +1,151 @@ +var svgEditorExtension_helloworld = (function () { + 'use strict'; + + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); + } + + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; + } + + function _iterableToArrayLimit(arr, i) { + if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { + return; + } + + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + + /** + * ext-helloworld.js + * + * @license MIT + * + * @copyright 2010 Alexis Deveria + * + */ + + /** + * This is a very basic SVG-Edit extension. It adds a "Hello World" button in + * the left ("mode") panel. Clicking on the button, and then the canvas + * will show the user the point on the canvas that was clicked on. + */ + var extHelloworld = { + name: 'helloworld', + init: function init(_ref) { + var $, importLocale, strings, svgEditor, svgCanvas; + return regeneratorRuntime.async(function init$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + $ = _ref.$, importLocale = _ref.importLocale; + _context.next = 3; + return regeneratorRuntime.awrap(importLocale()); + + case 3: + strings = _context.sent; + svgEditor = this; + svgCanvas = svgEditor.canvas; + return _context.abrupt("return", { + name: strings.name, + // For more notes on how to make an icon file, see the source of + // the helloworld-icon.xml + svgicons: svgEditor.curConfig.extIconsPath + 'helloworld-icon.xml', + // Multiple buttons can be added in this array + buttons: [{ + // Must match the icon ID in helloworld-icon.xml + id: 'hello_world', + // Fallback, e.g., for `file:///` access + icon: svgEditor.curConfig.extIconsPath + 'helloworld.png', + // This indicates that the button will be added to the "mode" + // button panel on the left side + type: 'mode', + // Tooltip text + title: strings.buttons[0].title, + // Events + events: { + click: function click() { + // The action taken when the button is clicked on. + // For "mode" buttons, any other button will + // automatically be de-pressed. + svgCanvas.setMode('hello_world'); + } + } + }], + // This is triggered when the main mouse button is pressed down + // on the editor canvas (not the tool panels) + mouseDown: function mouseDown() { + // Check the mode on mousedown + if (svgCanvas.getMode() === 'hello_world') { + // The returned object must include "started" with + // a value of true in order for mouseUp to be triggered + return { + started: true + }; + } + + return undefined; + }, + // This is triggered from anywhere, but "started" must have been set + // to true (see above). Note that "opts" is an object with event info + mouseUp: function mouseUp(opts) { + // Check the mode on mouseup + if (svgCanvas.getMode() === 'hello_world') { + var zoom = svgCanvas.getZoom(); // Get the actual coordinate by dividing by the zoom value + + var x = opts.mouse_x / zoom; + var y = opts.mouse_y / zoom; // We do our own formatting + + var text = strings.text; + [['x', x], ['y', y]].forEach(function (_ref2) { + var _ref3 = _slicedToArray(_ref2, 2), + prop = _ref3[0], + val = _ref3[1]; + + text = text.replace('{' + prop + '}', val); + }); // Show the text using the custom alert function + + $.alert(text); + } + } + }); + + case 7: + case "end": + return _context.stop(); + } + } + }, null, this); + } + }; + + return extHelloworld; + +}()); diff --git a/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-imagelib.js b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-imagelib.js new file mode 100644 index 00000000..caaa6301 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/dist/extensions/ext-imagelib.js @@ -0,0 +1,531 @@ +var svgEditorExtension_imagelib = (function () { + 'use strict'; + + function _typeof(obj) { + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function (obj) { + return typeof obj; + }; + } else { + _typeof = function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); + } + + /** + * ext-imagelib.js + * + * @license MIT + * + * @copyright 2010 Alexis Deveria + * + */ + var extImagelib = { + name: 'imagelib', + init: function init(_ref) { + var $, decode64, importLocale, dropXMLInternalSubset, imagelibStrings, modularVersion, svgEditor, uiStrings, svgCanvas, extIconsPath, allowedImageLibOrigins, closeBrowser, importImage, pending, mode, multiArr, transferStopped, preview, submit, toggleMulti, showBrowser, buttons; + return regeneratorRuntime.async(function init$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + showBrowser = function _ref9() { + var browser = $('#imgbrowse'); + + if (!browser.length) { + $('
      ' + '
      ').insertAfter('#svg_docprops'); + browser = $('#imgbrowse'); + var allLibs = imagelibStrings.select_lib; + var libOpts = $('
        ').appendTo(browser); + var frame = $(' + + + diff --git a/src/image_occlusion_enhanced/svg-edit/editor/browser.js b/src/image_occlusion_enhanced/svg-edit/editor/browser.js new file mode 100644 index 00000000..157d902d --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/editor/browser.js @@ -0,0 +1,285 @@ +/* globals jQuery */ +/** + * Browser detection + * @module browser + * @license MIT + * + * @copyright 2010 Jeff Schiller, 2010 Alexis Deveria + */ + +// Dependencies: +// 1) jQuery (for $.alert()) + +import './svgpathseg.js'; +import {NS} from './namespaces.js'; + +const $ = jQuery; + +const supportsSVG_ = (function () { +return Boolean(document.createElementNS && document.createElementNS(NS.SVG, 'svg').createSVGRect); +}()); + +/** + * @function module:browser.supportsSvg + * @returns {boolean} +*/ +export const supportsSvg = () => supportsSVG_; + +const {userAgent} = navigator; +const svg = document.createElementNS(NS.SVG, 'svg'); + +// Note: Browser sniffing should only be used if no other detection method is possible +const isOpera_ = Boolean(window.opera); +const isWebkit_ = userAgent.includes('AppleWebKit'); +const isGecko_ = userAgent.includes('Gecko/'); +const isIE_ = userAgent.includes('MSIE'); +const isChrome_ = userAgent.includes('Chrome/'); +const isWindows_ = userAgent.includes('Windows'); +const isMac_ = userAgent.includes('Macintosh'); +const isTouch_ = 'ontouchstart' in window; + +const supportsSelectors_ = (function () { +return Boolean(svg.querySelector); +}()); + +const supportsXpath_ = (function () { +return Boolean(document.evaluate); +}()); + +// segList functions (for FF1.5 and 2.0) +const supportsPathReplaceItem_ = (function () { +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 10,10'); +const seglist = path.pathSegList; +const seg = path.createSVGPathSegLinetoAbs(5, 5); +try { + seglist.replaceItem(seg, 1); + return true; +} catch (err) {} +return false; +}()); + +const supportsPathInsertItemBefore_ = (function () { +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 10,10'); +const seglist = path.pathSegList; +const seg = path.createSVGPathSegLinetoAbs(5, 5); +try { + seglist.insertItemBefore(seg, 1); + return true; +} catch (err) {} +return false; +}()); + +// text character positioning (for IE9 and now Chrome) +const supportsGoodTextCharPos_ = (function () { +const svgroot = document.createElementNS(NS.SVG, 'svg'); +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +document.documentElement.append(svgroot); +svgcontent.setAttribute('x', 5); +svgroot.append(svgcontent); +const text = document.createElementNS(NS.SVG, 'text'); +text.textContent = 'a'; +svgcontent.append(text); +try { // Chrome now fails here + const pos = text.getStartPositionOfChar(0).x; + return (pos === 0); +} catch (err) { + return false; +} finally { + svgroot.remove(); +} +}()); + +const supportsPathBBox_ = (function () { +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +document.documentElement.append(svgcontent); +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 C0,0 10,10 10,0'); +svgcontent.append(path); +const bbox = path.getBBox(); +svgcontent.remove(); +return (bbox.height > 4 && bbox.height < 5); +}()); + +// Support for correct bbox sizing on groups with horizontal/vertical lines +const supportsHVLineContainerBBox_ = (function () { +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +document.documentElement.append(svgcontent); +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 10,0'); +const path2 = document.createElementNS(NS.SVG, 'path'); +path2.setAttribute('d', 'M5,0 15,0'); +const g = document.createElementNS(NS.SVG, 'g'); +g.append(path, path2); +svgcontent.append(g); +const bbox = g.getBBox(); +svgcontent.remove(); +// Webkit gives 0, FF gives 10, Opera (correctly) gives 15 +return (bbox.width === 15); +}()); + +const supportsEditableText_ = (function () { +// TODO: Find better way to check support for this +return isOpera_; +}()); + +const supportsGoodDecimals_ = (function () { +// Correct decimals on clone attributes (Opera < 10.5/win/non-en) +const rect = document.createElementNS(NS.SVG, 'rect'); +rect.setAttribute('x', 0.1); +const crect = rect.cloneNode(false); +const retValue = (!crect.getAttribute('x').includes(',')); +if (!retValue) { + // Todo: i18nize or remove + $.alert( + 'NOTE: This version of Opera is known to contain bugs in SVG-edit.\n' + + 'Please upgrade to the latest version in which the problems have been fixed.' + ); +} +return retValue; +}()); + +const supportsNonScalingStroke_ = (function () { +const rect = document.createElementNS(NS.SVG, 'rect'); +rect.setAttribute('style', 'vector-effect:non-scaling-stroke'); +return rect.style.vectorEffect === 'non-scaling-stroke'; +}()); + +let supportsNativeSVGTransformLists_ = (function () { +const rect = document.createElementNS(NS.SVG, 'rect'); +const rxform = rect.transform.baseVal; +const t1 = svg.createSVGTransform(); +rxform.appendItem(t1); +const r1 = rxform.getItem(0); +const isSVGTransform = (o) => { + // https://developer.mozilla.org/en-US/docs/Web/API/SVGTransform + return o && typeof o === 'object' && typeof o.setMatrix === 'function' && 'angle' in o; +}; +return isSVGTransform(r1) && isSVGTransform(t1) && + r1.type === t1.type && r1.angle === t1.angle && + r1.matrix.a === t1.matrix.a && + r1.matrix.b === t1.matrix.b && + r1.matrix.c === t1.matrix.c && + r1.matrix.d === t1.matrix.d && + r1.matrix.e === t1.matrix.e && + r1.matrix.f === t1.matrix.f; +}()); + +// Public API + +/** + * @function module:browser.isOpera + * @returns {boolean} +*/ +export const isOpera = () => isOpera_; +/** + * @function module:browser.isWebkit + * @returns {boolean} +*/ +export const isWebkit = () => isWebkit_; +/** + * @function module:browser.isGecko + * @returns {boolean} +*/ +export const isGecko = () => isGecko_; +/** + * @function module:browser.isIE + * @returns {boolean} +*/ +export const isIE = () => isIE_; +/** + * @function module:browser.isChrome + * @returns {boolean} +*/ +export const isChrome = () => isChrome_; +/** + * @function module:browser.isWindows + * @returns {boolean} +*/ +export const isWindows = () => isWindows_; +/** + * @function module:browser.isMac + * @returns {boolean} +*/ +export const isMac = () => isMac_; +/** + * @function module:browser.isTouch + * @returns {boolean} +*/ +export const isTouch = () => isTouch_; + +/** + * @function module:browser.supportsSelectors + * @returns {boolean} +*/ +export const supportsSelectors = () => supportsSelectors_; + +/** + * @function module:browser.supportsXpath + * @returns {boolean} +*/ +export const supportsXpath = () => supportsXpath_; + +/** + * @function module:browser.supportsPathReplaceItem + * @returns {boolean} +*/ +export const supportsPathReplaceItem = () => supportsPathReplaceItem_; + +/** + * @function module:browser.supportsPathInsertItemBefore + * @returns {boolean} +*/ +export const supportsPathInsertItemBefore = () => supportsPathInsertItemBefore_; + +/** + * @function module:browser.supportsPathBBox + * @returns {boolean} +*/ +export const supportsPathBBox = () => supportsPathBBox_; + +/** + * @function module:browser.supportsHVLineContainerBBox + * @returns {boolean} +*/ +export const supportsHVLineContainerBBox = () => supportsHVLineContainerBBox_; + +/** + * @function module:browser.supportsGoodTextCharPos + * @returns {boolean} +*/ +export const supportsGoodTextCharPos = () => supportsGoodTextCharPos_; + +/** +* @function module:browser.supportsEditableText + * @returns {boolean} +*/ +export const supportsEditableText = () => supportsEditableText_; + +/** + * @function module:browser.supportsGoodDecimals + * @returns {boolean} +*/ +export const supportsGoodDecimals = () => supportsGoodDecimals_; + +/** +* @function module:browser.supportsNonScalingStroke +* @returns {boolean} +*/ +export const supportsNonScalingStroke = () => supportsNonScalingStroke_; + +/** +* @function module:browser.supportsNativeTransformLists +* @returns {boolean} +*/ +export const supportsNativeTransformLists = () => supportsNativeSVGTransformLists_; + +/** + * Set `supportsNativeSVGTransformLists_` to `false` (for unit testing). + * @function module:browser.disableSupportsNativeTransformLists + * @returns {void} +*/ +export const disableSupportsNativeTransformLists = () => { + supportsNativeSVGTransformLists_ = false; +}; diff --git a/src/image_occlusion_enhanced/svg-edit/editor/canvg/canvg.js b/src/image_occlusion_enhanced/svg-edit/editor/canvg/canvg.js new file mode 100644 index 00000000..6ce1d951 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/editor/canvg/canvg.js @@ -0,0 +1,2994 @@ +/* eslint-disable new-cap, class-methods-use-this */ +// Todo: Compare with latest canvg (add any improvements of ours) and add full JSDocs (denoting links to standard APIs and which are custom): https://github.com/canvg/canvg +/** + * canvg.js - Javascript SVG parser and renderer on Canvas + * @module canvg + * @license MIT + * @author Gabe Lerner + * @see https://github.com/canvg/canvg + */ + +import RGBColor from './rgbcolor.js'; +import {canvasRGBA} from '../external/stackblur-canvas/dist/stackblur-es.js'; + +/** + * Whether a value is `null` or `undefined`. + * @param {any} val + * @returns {boolean} + */ +const isNullish = (val) => { + return val === null || val === undefined; +}; + +/** +* @callback module:canvg.ForceRedraw +* @returns {boolean} +*/ + +/** +* @typedef {PlainObject} module:canvg.CanvgOptions +* @property {boolean} opts.ignoreMouse true => ignore mouse events +* @property {boolean} opts.ignoreAnimation true => ignore animations +* @property {boolean} opts.ignoreDimensions true => does not try to resize canvas +* @property {boolean} opts.ignoreClear true => does not clear canvas +* @property {Integer} opts.offsetX int => draws at a x offset +* @property {Integer} opts.offsetY int => draws at a y offset +* @property {Integer} opts.scaleWidth int => scales horizontally to width +* @property {Integer} opts.scaleHeight int => scales vertically to height +* @property {module:canvg.ForceRedraw} opts.forceRedraw function => will call the function on every frame, if it returns true, will redraw +* @property {boolean} opts.log Adds log function +* @property {boolean} opts.useCORS Whether to set CORS `crossOrigin` for the image to `Anonymous` +*/ + +/** +* If called with no arguments, it will replace all `` elements on the page +* with `` elements. +* @function module:canvg.canvg +* @param {HTMLCanvasElement|string} target canvas element or the id of a canvas element +* @param {string|XMLDocument} s - svg string, url to svg file, or xml document +* @param {module:canvg.CanvgOptions} [opts] Optional hash of options +* @returns {Promise} All the function after the first render is completed with dom +*/ +export const canvg = function (target, s, opts) { + // no parameters + if (isNullish(target) && isNullish(s) && isNullish(opts)) { + const svgTags = document.querySelectorAll('svg'); + return Promise.all([...svgTags].map((svgTag) => { + const c = document.createElement('canvas'); + c.width = svgTag.clientWidth; + c.height = svgTag.clientHeight; + svgTag.before(c); + svgTag.remove(); + const div = document.createElement('div'); + div.append(svgTag); + return canvg(c, div.innerHTML); + })); + } + + if (typeof target === 'string') { + target = document.getElementById(target); + } + + // store class on canvas + if (!isNullish(target.svg)) target.svg.stop(); + const svg = build(opts || {}); + // on i.e. 8 for flash canvas, we can't assign the property so check for it + if (!(target.childNodes.length === 1 && target.childNodes[0].nodeName === 'OBJECT')) { + target.svg = svg; + } + + const ctx = target.getContext('2d'); + if (typeof s.documentElement !== 'undefined') { + // load from xml doc + return svg.loadXmlDoc(ctx, s); + } + if (s.substr(0, 1) === '<') { + // load from xml string + return svg.loadXml(ctx, s); + } + // load from url + return svg.load(ctx, s); +}; + +/* eslint-disable jsdoc/check-types */ +/** +* @param {module:canvg.CanvgOptions} opts +* @returns {object} +* @todo Flesh out exactly what object is returned here (after updating to latest and reincluding our changes here and those of StackBlur) +*/ +function build (opts) { + /* eslint-enable jsdoc/check-types */ + const svg = {opts}; + + svg.FRAMERATE = 30; + svg.MAX_VIRTUAL_PIXELS = 30000; + + svg.log = function (msg) { /* */ }; + if (svg.opts.log === true && typeof console !== 'undefined') { + svg.log = function (msg) { console.log(msg); }; // eslint-disable-line no-console + } + + // globals + svg.init = function (ctx) { + let uniqueId = 0; + svg.UniqueId = function () { + uniqueId++; + return 'canvg' + uniqueId; + }; + svg.Definitions = {}; + svg.Styles = {}; + svg.Animations = []; + svg.Images = []; + svg.ctx = ctx; + svg.ViewPort = { + viewPorts: [], + Clear () { this.viewPorts = []; }, + SetCurrent (width, height) { this.viewPorts.push({width, height}); }, + RemoveCurrent () { this.viewPorts.pop(); }, + Current () { return this.viewPorts[this.viewPorts.length - 1]; }, + width () { return this.Current().width; }, + height () { return this.Current().height; }, + ComputeSize (d) { + if (!isNullish(d) && typeof d === 'number') return d; + if (d === 'x') return this.width(); + if (d === 'y') return this.height(); + return Math.sqrt( + (this.width() ** 2) + (this.height() ** 2) + ) / Math.sqrt(2); + } + }; + }; + svg.init(); + + // images loaded + svg.ImagesLoaded = function () { + return svg.Images.every((img) => img.loaded); + }; + + // trim + svg.trim = function (s) { + return s.replace(/^\s+|\s+$/g, ''); + }; + + // compress spaces + svg.compressSpaces = function (s) { + return s.replace(/[\s\r\t\n]+/gm, ' '); + }; + + // ajax + // Todo: Replace with `fetch` and polyfill + svg.ajax = function (url, asynch) { + const AJAX = window.XMLHttpRequest + ? new XMLHttpRequest() + : new window.ActiveXObject('Microsoft.XMLHTTP'); + if (asynch) { + return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new + const req = AJAX.open('GET', url, true); + req.addEventListener('load', () => { + resolve(AJAX.responseText); + }); + AJAX.send(null); + }); + } + + AJAX.open('GET', url, false); + AJAX.send(null); + return AJAX.responseText; + }; + + // parse xml + svg.parseXml = function (xml) { + if (window.DOMParser) { + const parser = new DOMParser(); + return parser.parseFromString(xml, 'text/xml'); + } + xml = xml.replace(/]*>/, ''); + const xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + }; + + // text extensions + // get the text baseline + const textBaselineMapping = { + baseline: 'alphabetic', + 'before-edge': 'top', + 'text-before-edge': 'top', + middle: 'middle', + central: 'middle', + 'after-edge': 'bottom', + 'text-after-edge': 'bottom', + ideographic: 'ideographic', + alphabetic: 'alphabetic', + hanging: 'hanging', + mathematical: 'alphabetic' + }; + + svg.Property = class Property { + constructor (name, value) { + this.name = name; + this.value = value; + } + + getValue () { + return this.value; + } + + hasValue () { + return (!isNullish(this.value) && this.value !== ''); + } + + // return the numerical value of the property + numValue () { + if (!this.hasValue()) return 0; + + let n = parseFloat(this.value); + if (String(this.value).endsWith('%')) { + n /= 100.0; + } + return n; + } + + valueOrDefault (def) { + if (this.hasValue()) return this.value; + return def; + } + + numValueOrDefault (def) { + if (this.hasValue()) return this.numValue(); + return def; + } + + // color extensions + // augment the current color value with the opacity + addOpacity (opacityProp) { + let newValue = this.value; + if (!isNullish(opacityProp.value) && opacityProp.value !== '' && typeof this.value === 'string') { // can only add opacity to colors, not patterns + const color = new RGBColor(this.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')'; + } + } + return new svg.Property(this.name, newValue); + } + + // definition extensions + // get the definition from the definitions table + getDefinition () { + let name = this.value.match(/#([^)'"]+)/); + if (name) { name = name[1]; } + if (!name) { name = this.value; } + return svg.Definitions[name]; + } + + isUrlDefinition () { + return this.value.startsWith('url('); + } + + getFillStyleDefinition (e, opacityProp) { + let def = this.getDefinition(); + + // gradient + if (!isNullish(def) && def.createGradient) { + return def.createGradient(svg.ctx, e, opacityProp); + } + + // pattern + if (!isNullish(def) && def.createPattern) { + if (def.getHrefAttribute().hasValue()) { + const pt = def.attribute('patternTransform'); + def = def.getHrefAttribute().getDefinition(); + if (pt.hasValue()) { def.attribute('patternTransform', true).value = pt.value; } + } + return def.createPattern(svg.ctx, e); + } + + return null; + } + + // length extensions + getDPI (viewPort) { + return 96.0; // TODO: compute? + } + + getEM (viewPort) { + let em = 12; + + const fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + if (fontSize.hasValue()) em = fontSize.toPixels(viewPort); + + return em; + } + + getUnits () { + return String(this.value).replace(/[0-9.-]/g, ''); + } + + // get the length as pixels + toPixels (viewPort, processPercent) { + if (!this.hasValue()) return 0; + const s = String(this.value); + if (s.endsWith('em')) return this.numValue() * this.getEM(viewPort); + if (s.endsWith('ex')) return this.numValue() * this.getEM(viewPort) / 2.0; + if (s.endsWith('px')) return this.numValue(); + if (s.endsWith('pt')) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); + if (s.endsWith('pc')) return this.numValue() * 15; + if (s.endsWith('cm')) return this.numValue() * this.getDPI(viewPort) / 2.54; + if (s.endsWith('mm')) return this.numValue() * this.getDPI(viewPort) / 25.4; + if (s.endsWith('in')) return this.numValue() * this.getDPI(viewPort); + if (s.endsWith('%')) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); + const n = this.numValue(); + if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); + return n; + } + + // time extensions + // get the time as milliseconds + toMilliseconds () { + if (!this.hasValue()) return 0; + const s = String(this.value); + if (s.endsWith('ms')) return this.numValue(); + if (s.endsWith('s')) return this.numValue() * 1000; + return this.numValue(); + } + + // angle extensions + // get the angle as radians + toRadians () { + if (!this.hasValue()) return 0; + const s = String(this.value); + if (s.endsWith('deg')) return this.numValue() * (Math.PI / 180.0); + if (s.endsWith('grad')) return this.numValue() * (Math.PI / 200.0); + if (s.endsWith('rad')) return this.numValue(); + return this.numValue() * (Math.PI / 180.0); + } + + toTextBaseline () { + if (!this.hasValue()) return null; + return textBaselineMapping[this.value]; + } + }; + + // fonts + svg.Font = { + Styles: 'normal|italic|oblique|inherit', + Variants: 'normal|small-caps|inherit', + Weights: 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit', + + CreateFont (fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { + const f = !isNullish(inherit) ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + return { + fontFamily: fontFamily || f.fontFamily, + fontSize: fontSize || f.fontSize, + fontStyle: fontStyle || f.fontStyle, + fontWeight: fontWeight || f.fontWeight, + fontVariant: fontVariant || f.fontVariant, + toString () { + return [ + this.fontStyle, this.fontVariant, this.fontWeight, + this.fontSize, this.fontFamily + ].join(' '); + } + }; + }, + + Parse (s) { + const f = {}; + const ds = svg.trim(svg.compressSpaces(s || '')).split(' '); + const set = { + fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false + }; + let ff = ''; + ds.forEach((d) => { + if (!set.fontStyle && this.Styles.includes(d)) { + if (d !== 'inherit') { + f.fontStyle = d; + } + set.fontStyle = true; + } else if (!set.fontVariant && this.Variants.includes(d)) { + if (d !== 'inherit') { + f.fontVariant = d; + } + set.fontStyle = set.fontVariant = true; + } else if (!set.fontWeight && this.Weights.includes(d)) { + if (d !== 'inherit') { + f.fontWeight = d; + } + set.fontStyle = set.fontVariant = set.fontWeight = true; + } else if (!set.fontSize) { + if (d !== 'inherit') { + f.fontSize = d.split('/')[0]; + } + set.fontStyle = set.fontVariant = set.fontWeight = set.fontSize = true; + } else if (d !== 'inherit') { + ff += d; + } + }); + if (ff !== '') { f.fontFamily = ff; } + return f; + } + }; + + // points and paths + svg.ToNumberArray = function (s) { + const a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' '); + return a.map((_a) => parseFloat(_a)); + }; + svg.Point = class { + constructor (x, y) { + this.x = x; + this.y = y; + } + + angleTo (p) { + return Math.atan2(p.y - this.y, p.x - this.x); + } + + applyTransform (v) { + const xp = this.x * v[0] + this.y * v[2] + v[4]; + const yp = this.x * v[1] + this.y * v[3] + v[5]; + this.x = xp; + this.y = yp; + } + }; + + svg.CreatePoint = function (s) { + const a = svg.ToNumberArray(s); + return new svg.Point(a[0], a[1]); + }; + svg.CreatePath = function (s) { + const a = svg.ToNumberArray(s); + const path = []; + for (let i = 0; i < a.length; i += 2) { + path.push(new svg.Point(a[i], a[i + 1])); + } + return path; + }; + + // bounding box + svg.BoundingBox = class { + constructor (x1, y1, x2, y2) { // pass in initial points if you want + this.x1 = Number.NaN; + this.y1 = Number.NaN; + this.x2 = Number.NaN; + this.y2 = Number.NaN; + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + + x () { return this.x1; } + y () { return this.y1; } + width () { return this.x2 - this.x1; } + height () { return this.y2 - this.y1; } + + addPoint (x, y) { + if (!isNullish(x)) { + if (isNaN(this.x1) || isNaN(this.x2)) { + this.x1 = x; + this.x2 = x; + } + if (x < this.x1) this.x1 = x; + if (x > this.x2) this.x2 = x; + } + + if (!isNullish(y)) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + } + addX (x) { this.addPoint(x, null); } + addY (y) { this.addPoint(null, y); } + + addBoundingBox (bb) { + this.addPoint(bb.x1, bb.y1); + this.addPoint(bb.x2, bb.y2); + } + + addQuadraticCurve (p0x, p0y, p1x, p1y, p2x, p2y) { + const cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + const cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + const cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + const cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + + addBezierCurve (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + const p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + for (let i = 0; i <= 1; i++) { + const f = function (t) { + return ((1 - t) ** 3) * p0[i] + + 3 * ((1 - t) ** 2) * t * p1[i] + + 3 * (1 - t) * (t ** 2) * p2[i] + + (t ** 3) * p3[i]; + }; + + const b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + const a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + const c = 3 * p1[i] - 3 * p0[i]; + + if (a === 0) { + if (b === 0) continue; + const t = -c / b; + if (t > 0 && t < 1) { + if (i === 0) this.addX(f(t)); + if (i === 1) this.addY(f(t)); + } + continue; + } + + const b2ac = (b ** 2) - 4 * c * a; + if (b2ac < 0) continue; + const t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (t1 > 0 && t1 < 1) { + if (i === 0) this.addX(f(t1)); + if (i === 1) this.addY(f(t1)); + } + const t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (t2 > 0 && t2 < 1) { + if (i === 0) this.addX(f(t2)); + if (i === 1) this.addY(f(t2)); + } + } + } + + isPointInBox (x, y) { + return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); + } + }; + + // transforms + svg.Transform = class { + constructor (v) { + this.Type = { + translate: class { + constructor (s) { + this.p = svg.CreatePoint(s); + this.apply = function (ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + }; + this.unapply = function (ctx) { + ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0); + }; + this.applyToPoint = function (p) { + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + }; + } + }, + rotate: class { + constructor (s) { + const a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + this.apply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + this.unapply = function (ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(-1.0 * this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + }; + this.applyToPoint = function (p) { + const _a = this.angle.toRadians(); + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + p.applyTransform([Math.cos(_a), Math.sin(_a), -Math.sin(_a), Math.cos(_a), 0, 0]); + p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); + }; + } + }, + scale: class { + constructor (s) { + this.p = svg.CreatePoint(s); + this.apply = function (ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + }; + this.unapply = function (ctx) { + ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0); + }; + this.applyToPoint = function (p) { + p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); + }; + } + }, + matrix: class { + constructor (s) { + this.m = svg.ToNumberArray(s); + this.apply = function (ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + }; + this.applyToPoint = function (p) { + p.applyTransform(this.m); + }; + } + } + }; + Object.assign(this.Type, { + SkewBase: class extends this.Type.matrix { + constructor (s) { + super(s); + this.angle = new svg.Property('angle', s); + } + } + }); + Object.assign(this.Type, { + skewX: class extends this.Type.SkewBase { + constructor (s) { + super(s); + this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0]; + } + }, + skewY: class extends this.Type.SkewBase { + constructor (s) { + super(s); + this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0]; + } + } + }); + + const data = svg.trim(svg.compressSpaces(v)).replace(/\)([a-zA-Z])/g, ') $1').replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/); + this.transforms = data.map((d) => { + const type = svg.trim(d.split('(')[0]); + const s = d.split('(')[1].replace(')', ''); + const transform = new this.Type[type](s); + transform.type = type; + return transform; + }); + } + + apply (ctx) { + this.transforms.forEach((transform) => { + transform.apply(ctx); + }); + } + + unapply (ctx) { + for (let i = this.transforms.length - 1; i >= 0; i--) { + this.transforms[i].unapply(ctx); + } + } + + applyToPoint (p) { + this.transforms.forEach((transform) => { + transform.applyToPoint(p); + }); + } + }; + + // aspect ratio + svg.AspectRatio = function (ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX, minY, refX, refY) { + // aspect ratio - https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + aspectRatio = svg.compressSpaces(aspectRatio); + aspectRatio = aspectRatio.replace(/^defer\s/, ''); // ignore defer + const align = aspectRatio.split(' ')[0] || 'xMidYMid'; + const meetOrSlice = aspectRatio.split(' ')[1] || 'meet'; + + // calculate scale + const scaleX = width / desiredWidth; + const scaleY = height / desiredHeight; + const scaleMin = Math.min(scaleX, scaleY); + const scaleMax = Math.max(scaleX, scaleY); + if (meetOrSlice === 'meet') { desiredWidth *= scaleMin; desiredHeight *= scaleMin; } + if (meetOrSlice === 'slice') { desiredWidth *= scaleMax; desiredHeight *= scaleMax; } + + refX = new svg.Property('refX', refX); + refY = new svg.Property('refY', refY); + if (refX.hasValue() && refY.hasValue()) { + ctx.translate(-scaleMin * refX.toPixels('x'), -scaleMin * refY.toPixels('y')); + } else { + // align + if (align.startsWith('xMid') && ((meetOrSlice === 'meet' && scaleMin === scaleY) || (meetOrSlice === 'slice' && scaleMax === scaleY))) ctx.translate(width / 2.0 - desiredWidth / 2.0, 0); + if (align.endsWith('YMid') && ((meetOrSlice === 'meet' && scaleMin === scaleX) || (meetOrSlice === 'slice' && scaleMax === scaleX))) ctx.translate(0, height / 2.0 - desiredHeight / 2.0); + if (align.startsWith('xMax') && ((meetOrSlice === 'meet' && scaleMin === scaleY) || (meetOrSlice === 'slice' && scaleMax === scaleY))) ctx.translate(width - desiredWidth, 0); + if (align.endsWith('YMax') && ((meetOrSlice === 'meet' && scaleMin === scaleX) || (meetOrSlice === 'slice' && scaleMax === scaleX))) ctx.translate(0, height - desiredHeight); + } + + // scale + if (align === 'none') ctx.scale(scaleX, scaleY); + else if (meetOrSlice === 'meet') ctx.scale(scaleMin, scaleMin); + else if (meetOrSlice === 'slice') ctx.scale(scaleMax, scaleMax); + + // translate + ctx.translate(isNullish(minX) ? 0 : -minX, isNullish(minY) ? 0 : -minY); + }; + + // elements + svg.Element = {}; + + svg.EmptyProperty = new svg.Property('EMPTY', ''); + + svg.Element.ElementBase = class { + constructor (node) { + // Argument from inheriting class + this.captureTextNodes = arguments[1]; // eslint-disable-line prefer-rest-params + this.attributes = {}; + this.styles = {}; + this.children = []; + if (!isNullish(node) && node.nodeType === 1) { // ELEMENT_NODE + // add children + [...node.childNodes].forEach((childNode) => { + if (childNode.nodeType === 1) { + this.addChild(childNode, true); // ELEMENT_NODE + } + if (this.captureTextNodes && ( + childNode.nodeType === 3 || childNode.nodeType === 4 + )) { + const text = childNode.nodeValue || childNode.text || ''; + if (svg.trim(svg.compressSpaces(text)) !== '') { + this.addChild(new svg.Element.tspan(childNode), false); // TEXT_NODE + } + } + }); + + // add attributes + [...node.attributes].forEach(({nodeName, nodeValue}) => { + this.attributes[nodeName] = new svg.Property( + nodeName, + nodeValue + ); + }); + // add tag styles + let styles = svg.Styles[node.nodeName]; + if (!isNullish(styles)) { + Object.entries(styles).forEach(([name, styleValue]) => { + this.styles[name] = styleValue; + }); + } + + // add class styles + if (this.attribute('class').hasValue()) { + const classes = svg.compressSpaces(this.attribute('class').value).split(' '); + classes.forEach((clss) => { + styles = svg.Styles['.' + clss]; + if (!isNullish(styles)) { + Object.entries(styles).forEach(([name, styleValue]) => { + this.styles[name] = styleValue; + }); + } + styles = svg.Styles[node.nodeName + '.' + clss]; + if (!isNullish(styles)) { + Object.entries(styles).forEach(([name, styleValue]) => { + this.styles[name] = styleValue; + }); + } + }); + } + + // add id styles + if (this.attribute('id').hasValue()) { + const _styles = svg.Styles['#' + this.attribute('id').value]; + if (!isNullish(_styles)) { + Object.entries(_styles).forEach(([name, styleValue]) => { + this.styles[name] = styleValue; + }); + } + } + + // add inline styles + if (this.attribute('style').hasValue()) { + const _styles = this.attribute('style').value.split(';'); + _styles.forEach((style) => { + if (svg.trim(style) !== '') { + let {name, value} = style.split(':'); + name = svg.trim(name); + value = svg.trim(value); + this.styles[name] = new svg.Property(name, value); + } + }); + } + + // add id + if (this.attribute('id').hasValue()) { + if (isNullish(svg.Definitions[this.attribute('id').value])) { + svg.Definitions[this.attribute('id').value] = this; + } + } + } + } + + // get or create attribute + attribute (name, createIfNotExists) { + let a = this.attributes[name]; + if (!isNullish(a)) return a; + + if (createIfNotExists === true) { a = new svg.Property(name, ''); this.attributes[name] = a; } + return a || svg.EmptyProperty; + } + + getHrefAttribute () { + for (const a in this.attributes) { + if (a.endsWith(':href')) { + return this.attributes[a]; + } + } + return svg.EmptyProperty; + } + + // get or create style, crawls up node tree + style (name, createIfNotExists, skipAncestors) { + let s = this.styles[name]; + if (!isNullish(s)) return s; + + const a = this.attribute(name); + if (!isNullish(a) && a.hasValue()) { + this.styles[name] = a; // move up to me to cache + return a; + } + + if (skipAncestors !== true) { + const p = this.parent; + if (!isNullish(p)) { + const ps = p.style(name); + if (!isNullish(ps) && ps.hasValue()) { + return ps; + } + } + } + + if (createIfNotExists === true) { s = new svg.Property(name, ''); this.styles[name] = s; } + return s || svg.EmptyProperty; + } + + // base render + render (ctx) { + // don't render display=none + if (this.style('display').value === 'none') return; + + // don't render visibility=hidden + if (this.style('visibility').value === 'hidden') return; + + ctx.save(); + if (this.attribute('mask').hasValue()) { // mask + const mask = this.attribute('mask').getDefinition(); + if (!isNullish(mask)) mask.apply(ctx, this); + } else if (this.style('filter').hasValue()) { // filter + const filter = this.style('filter').getDefinition(); + if (!isNullish(filter)) filter.apply(ctx, this); + } else { + this.setContext(ctx); + this.renderChildren(ctx); + this.clearContext(ctx); + } + ctx.restore(); + } + + // base set context + setContext (ctx) { + // OVERRIDE ME! + } + + // base clear context + clearContext (ctx) { + // OVERRIDE ME! + } + + // base render children + renderChildren (ctx) { + this.children.forEach((child) => { + child.render(ctx); + }); + } + + addChild (childNode, create) { + const child = create + ? svg.CreateElement(childNode) + : childNode; + child.parent = this; + if (child.type !== 'title') { this.children.push(child); } + } + }; + + svg.Element.RenderedElementBase = class extends svg.Element.ElementBase { + setContext (ctx) { + // fill + if (this.style('fill').isUrlDefinition()) { + const fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity')); + if (!isNullish(fs)) ctx.fillStyle = fs; + } else if (this.style('fill').hasValue()) { + const fillStyle = this.style('fill'); + if (fillStyle.value === 'currentColor') fillStyle.value = this.style('color').value; + ctx.fillStyle = (fillStyle.value === 'none' ? 'rgba(0,0,0,0)' : fillStyle.value); + } + if (this.style('fill-opacity').hasValue()) { + let fillStyle = new svg.Property('fill', ctx.fillStyle); + fillStyle = fillStyle.addOpacity(this.style('fill-opacity')); + ctx.fillStyle = fillStyle.value; + } + + // stroke + if (this.style('stroke').isUrlDefinition()) { + const fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity')); + if (!isNullish(fs)) ctx.strokeStyle = fs; + } else if (this.style('stroke').hasValue()) { + const strokeStyle = this.style('stroke'); + if (strokeStyle.value === 'currentColor') strokeStyle.value = this.style('color').value; + ctx.strokeStyle = (strokeStyle.value === 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value); + } + if (this.style('stroke-opacity').hasValue()) { + let strokeStyle = new svg.Property('stroke', ctx.strokeStyle); + strokeStyle = strokeStyle.addOpacity(this.style('stroke-opacity')); + ctx.strokeStyle = strokeStyle.value; + } + if (this.style('stroke-width').hasValue()) { + const newLineWidth = this.style('stroke-width').toPixels(); + ctx.lineWidth = newLineWidth === 0 ? 0.001 : newLineWidth; // browsers don't respect 0 + } + if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value; + if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value; + if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value; + if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value !== 'none') { + const gaps = svg.ToNumberArray(this.style('stroke-dasharray').value); + if (typeof ctx.setLineDash !== 'undefined') { + ctx.setLineDash(gaps); + } else if (typeof ctx.webkitLineDash !== 'undefined') { + ctx.webkitLineDash = gaps; + } else if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) { + ctx.mozDash = gaps; + } + + const offset = this.style('stroke-dashoffset').numValueOrDefault(1); + if (typeof ctx.lineDashOffset !== 'undefined') { + ctx.lineDashOffset = offset; + } else if (typeof ctx.webkitLineDashOffset !== 'undefined') { + ctx.webkitLineDashOffset = offset; + } else if (typeof ctx.mozDashOffset !== 'undefined') { + ctx.mozDashOffset = offset; + } + } + + // font + if (typeof ctx.font !== 'undefined') { + ctx.font = svg.Font.CreateFont( + this.style('font-style').value, + this.style('font-variant').value, + this.style('font-weight').value, + this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '', + this.style('font-family').value + ).toString(); + } + + // transform + if (this.attribute('transform').hasValue()) { + const transform = new svg.Transform(this.attribute('transform').value); + transform.apply(ctx); + } + + // clip + if (this.style('clip-path', false, true).hasValue()) { + const clip = this.style('clip-path', false, true).getDefinition(); + if (!isNullish(clip)) clip.apply(ctx); + } + + // opacity + if (this.style('opacity').hasValue()) { + ctx.globalAlpha = this.style('opacity').numValue(); + } + } + }; + + svg.Element.PathElementBase = class extends svg.Element.RenderedElementBase { + path (ctx) { + if (!isNullish(ctx)) ctx.beginPath(); + return new svg.BoundingBox(); + } + + renderChildren (ctx) { + this.path(ctx); + svg.Mouse.checkPath(this, ctx); + if (ctx.fillStyle !== '') { + if (this.style('fill-rule').valueOrDefault('inherit') !== 'inherit') { + ctx.fill(this.style('fill-rule').value); + } else { + ctx.fill(); + } + } + if (ctx.strokeStyle !== '') ctx.stroke(); + + const markers = this.getMarkers(); + if (!isNullish(markers)) { + if (this.style('marker-start').isUrlDefinition()) { + const marker = this.style('marker-start').getDefinition(); + marker.render(ctx, markers[0][0], markers[0][1]); + } + if (this.style('marker-mid').isUrlDefinition()) { + const marker = this.style('marker-mid').getDefinition(); + for (let i = 1; i < markers.length - 1; i++) { + marker.render(ctx, markers[i][0], markers[i][1]); + } + } + if (this.style('marker-end').isUrlDefinition()) { + const marker = this.style('marker-end').getDefinition(); + marker.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]); + } + } + } + + getBoundingBox () { + return this.path(); + } + + getMarkers () { + return null; + } + }; + + // svg element + svg.Element.svg = class extends svg.Element.RenderedElementBase { + clearContext (ctx) { + super.clearContext(ctx); + svg.ViewPort.RemoveCurrent(); + } + + setContext (ctx) { + // initial values and defaults + ctx.strokeStyle = 'rgba(0,0,0,0)'; + ctx.lineCap = 'butt'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 4; + if (typeof ctx.font !== 'undefined' && typeof window.getComputedStyle !== 'undefined') { + ctx.font = window.getComputedStyle(ctx.canvas).getPropertyValue('font'); + } + + super.setContext(ctx); + + // create new view port + if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0; + if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0; + ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y')); + + let width = svg.ViewPort.width(); + let height = svg.ViewPort.height(); + + if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%'; + if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%'; + if (typeof this.root === 'undefined') { + width = this.attribute('width').toPixels('x'); + height = this.attribute('height').toPixels('y'); + + let x = 0; + let y = 0; + if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) { + x = -this.attribute('refX').toPixels('x'); + y = -this.attribute('refY').toPixels('y'); + } + + if (this.attribute('overflow').valueOrDefault('hidden') !== 'visible') { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(width, y); + ctx.lineTo(width, height); + ctx.lineTo(x, height); + ctx.closePath(); + ctx.clip(); + } + } + svg.ViewPort.SetCurrent(width, height); + + // viewbox + if (this.attribute('viewBox').hasValue()) { + const viewBox = svg.ToNumberArray(this.attribute('viewBox').value); + const minX = viewBox[0]; + const minY = viewBox[1]; + width = viewBox[2]; + height = viewBox[3]; + + svg.AspectRatio( + ctx, + this.attribute('preserveAspectRatio').value, + svg.ViewPort.width(), + width, + svg.ViewPort.height(), + height, + minX, + minY, + this.attribute('refX').value, + this.attribute('refY').value + ); + + svg.ViewPort.RemoveCurrent(); + svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); + } + } + }; + + // rect element + svg.Element.rect = class extends svg.Element.PathElementBase { + path (ctx) { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + const width = this.attribute('width').toPixels('x'); + const height = this.attribute('height').toPixels('y'); + let rx = this.attribute('rx').toPixels('x'); + let ry = this.attribute('ry').toPixels('y'); + if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx; + if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry; + rx = Math.min(rx, width / 2.0); + ry = Math.min(ry, height / 2.0); + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.moveTo(x + rx, y); + ctx.lineTo(x + width - rx, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + ry); + ctx.lineTo(x + width, y + height - ry); + ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); + ctx.lineTo(x + rx, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - ry); + ctx.lineTo(x, y + ry); + ctx.quadraticCurveTo(x, y, x + rx, y); + ctx.closePath(); + } + + return new svg.BoundingBox(x, y, x + width, y + height); + } + }; + + // circle element + svg.Element.circle = class extends svg.Element.PathElementBase { + path (ctx) { + const cx = this.attribute('cx').toPixels('x'); + const cy = this.attribute('cy').toPixels('y'); + const r = this.attribute('r').toPixels(); + + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2, true); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r); + } + }; + + // ellipse element + const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + svg.Element.ellipse = class extends svg.Element.PathElementBase { + path (ctx) { + const rx = this.attribute('rx').toPixels('x'); + const ry = this.attribute('ry').toPixels('y'); + const cx = this.attribute('cx').toPixels('x'); + const cy = this.attribute('cy').toPixels('y'); + + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.moveTo(cx, cy - ry); + ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy); + ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry); + ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy); + ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); + } + }; + + // line element + svg.Element.line = class extends svg.Element.PathElementBase { + getPoints () { + return [ + new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')), + new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y')) + ]; + } + + path (ctx) { + const points = this.getPoints(); + + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + ctx.lineTo(points[1].x, points[1].y); + } + + return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y); + } + + getMarkers () { + const points = this.getPoints(); + const a = points[0].angleTo(points[1]); + return [[points[0], a], [points[1], a]]; + } + }; + + // polyline element + svg.Element.polyline = class extends svg.Element.PathElementBase { + constructor (node) { + super(node); + + this.points = svg.CreatePath(this.attribute('points').value); + } + path (ctx) { + const {x, y} = this.points[0]; + const bb = new svg.BoundingBox(x, y); + if (!isNullish(ctx)) { + ctx.beginPath(); + ctx.moveTo(x, y); + } + for (let i = 1; i < this.points.length; i++) { + const {x: _x, y: _y} = this.points[i]; + bb.addPoint(_x, _y); + if (!isNullish(ctx)) ctx.lineTo(_x, _y); + } + return bb; + } + + getMarkers () { + const markers = []; + for (let i = 0; i < this.points.length - 1; i++) { + markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]); + } + markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]); + return markers; + } + }; + + // polygon element + svg.Element.polygon = class extends svg.Element.polyline { + path (ctx) { + const bb = super.path(ctx); + if (!isNullish(ctx)) { + ctx.lineTo(this.points[0].x, this.points[0].y); + ctx.closePath(); + } + return bb; + } + }; + + // path element + svg.Element.path = class extends svg.Element.PathElementBase { + constructor (node) { + super(node); + + let d = this.attribute('d').value + // TODO: convert to real lexer based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF + .replace(/,/gm, ' ') // get rid of all commas + .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands + .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands + .replace(/([MmZzLlHhVvCcSsQqTtAa])([^\s])/gm, '$1 $2') // separate commands from points + .replace(/([^\s])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from points + .replace(/(\d)([+-])/gm, '$1 $2') // separate digits when no comma + .replace(/(\.\d*)(\.)/gm, '$1 $2') // separate digits when no comma + .replace(/([Aa](\s+\d+)(\s+\d+)(\s+\d+))\s+([01])\s*([01])/gm, '$1 $5 $6 '); // shorthand elliptical arc path syntax + d = svg.compressSpaces(d); // compress multiple spaces + d = svg.trim(d); + this.PathParser = { + tokens: d.split(' '), + + reset () { + this.i = -1; + this.command = ''; + this.previousCommand = ''; + this.start = new svg.Point(0, 0); + this.control = new svg.Point(0, 0); + this.current = new svg.Point(0, 0); + this.points = []; + this.angles = []; + }, + + isEnd () { + return this.i >= this.tokens.length - 1; + }, + + isCommandOrEnd () { + if (this.isEnd()) return true; + return !isNullish(this.tokens[this.i + 1].match(/^[A-Za-z]$/)); + }, + + isRelativeCommand () { + switch (this.command) { + case 'm': + case 'l': + case 'h': + case 'v': + case 'c': + case 's': + case 'q': + case 't': + case 'a': + case 'z': + return true; + } + return false; + }, + + getToken () { + this.i++; + return this.tokens[this.i]; + }, + + getScalar () { + return parseFloat(this.getToken()); + }, + + nextCommand () { + this.previousCommand = this.command; + this.command = this.getToken(); + }, + + getPoint () { + const p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + }, + + getAsControlPoint () { + const p = this.getPoint(); + this.control = p; + return p; + }, + + getAsCurrentPoint () { + const p = this.getPoint(); + this.current = p; + return p; + }, + + getReflectedControlPoint () { + if (this.previousCommand.toLowerCase() !== 'c' && + this.previousCommand.toLowerCase() !== 's' && + this.previousCommand.toLowerCase() !== 'q' && + this.previousCommand.toLowerCase() !== 't') { + return this.current; + } + + // reflect point + const p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + }, + + makeAbsolute (p) { + if (this.isRelativeCommand()) { + p.x += this.current.x; + p.y += this.current.y; + } + return p; + }, + + addMarker (p, from, priorTo) { + // if the last angle isn't filled in because we didn't have this point yet ... + if (!isNullish(priorTo) && this.angles.length > 0 && isNullish(this.angles[this.angles.length - 1])) { + this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo); + } + this.addMarkerAngle(p, isNullish(from) ? null : from.angleTo(p)); + }, + + addMarkerAngle (p, a) { + this.points.push(p); + this.angles.push(a); + }, + + getMarkerPoints () { return this.points; }, + getMarkerAngles () { + for (let i = 0; i < this.angles.length; i++) { + if (isNullish(this.angles[i])) { + for (let j = i + 1; j < this.angles.length; j++) { + if (!isNullish(this.angles[j])) { + this.angles[i] = this.angles[j]; + break; + } + } + } + } + return this.angles; + } + }; + } + + path (ctx) { + const pp = this.PathParser; + pp.reset(); + + const bb = new svg.BoundingBox(); + if (!isNullish(ctx)) ctx.beginPath(); + while (!pp.isEnd()) { + pp.nextCommand(); + switch (pp.command) { + case 'M': + case 'm': { + const p = pp.getAsCurrentPoint(); + pp.addMarker(p); + bb.addPoint(p.x, p.y); + if (!isNullish(ctx)) ctx.moveTo(p.x, p.y); + pp.start = pp.current; + while (!pp.isCommandOrEnd()) { + const _p = pp.getAsCurrentPoint(); + pp.addMarker(_p, pp.start); + bb.addPoint(_p.x, _p.y); + if (!isNullish(ctx)) ctx.lineTo(_p.x, _p.y); + } + break; + } case 'L': + case 'l': + while (!pp.isCommandOrEnd()) { + const c = pp.current; + const p = pp.getAsCurrentPoint(); + pp.addMarker(p, c); + bb.addPoint(p.x, p.y); + if (!isNullish(ctx)) ctx.lineTo(p.x, p.y); + } + break; + case 'H': + case 'h': + while (!pp.isCommandOrEnd()) { + const newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (!isNullish(ctx)) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'V': + case 'v': + while (!pp.isCommandOrEnd()) { + const newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (!isNullish(ctx)) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'C': + case 'c': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const p1 = pp.getPoint(); + const cntrl = pp.getAsControlPoint(); + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (!isNullish(ctx)) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'S': + case 's': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const p1 = pp.getReflectedControlPoint(); + const cntrl = pp.getAsControlPoint(); + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (!isNullish(ctx)) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'Q': + case 'q': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const cntrl = pp.getAsControlPoint(); + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (!isNullish(ctx)) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'T': + case 't': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const cntrl = pp.getReflectedControlPoint(); + pp.control = cntrl; + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (!isNullish(ctx)) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'A': + case 'a': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + let rx = pp.getScalar(); + let ry = pp.getScalar(); + const xAxisRotation = pp.getScalar() * (Math.PI / 180.0); + const largeArcFlag = pp.getScalar(); + const sweepFlag = pp.getScalar(); + const cp = pp.getAsCurrentPoint(); + + // Conversion from endpoint to center parameterization + // https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter + + // x1', y1' + const currp = new svg.Point( + Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, + -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0 + ); + // adjust radii + const l = (currp.x ** 2) / (rx ** 2) + (currp.y ** 2) / (ry ** 2); + if (l > 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + let s = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt( + (((rx ** 2) * (ry ** 2)) - ((rx ** 2) * (currp.y ** 2)) - ((ry ** 2) * (currp.x ** 2))) / + ((rx ** 2) * (currp.y ** 2) + (ry ** 2) * (currp.x ** 2)) + ); + if (isNaN(s)) s = 0; + const cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + const centp = new svg.Point( + (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, + (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y + ); + // vector magnitude + const m = function (v) { + return Math.sqrt((v[0] ** 2) + (v[1] ** 2)); + }; + // ratio between two vectors + const r = function (u, v) { + return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)); + }; + // angle between two vectors + const a = function (u, v) { + return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); + }; + // initial angle + const a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]); + // angle delta + const u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]; + const v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry]; + let ad = a(u, v); + if (r(u, v) <= -1) ad = Math.PI; + if (r(u, v) >= 1) ad = 0; + + // for markers + const dir = 1 - sweepFlag ? 1.0 : -1.0; + const ah = a1 + dir * (ad / 2.0); + const halfWay = new svg.Point( + centp.x + rx * Math.cos(ah), + centp.y + ry * Math.sin(ah) + ); + pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); + pp.addMarkerAngle(cp, ah - dir * Math.PI); + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (!isNullish(ctx)) { + const _r = rx > ry ? rx : ry; + const sx = rx > ry ? 1 : rx / ry; + const sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, _r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1 / sx, 1 / sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + break; + case 'Z': + case 'z': + if (!isNullish(ctx)) ctx.closePath(); + pp.current = pp.start; + } + } + + return bb; + } + + getMarkers () { + const points = this.PathParser.getMarkerPoints(); + const angles = this.PathParser.getMarkerAngles(); + + const markers = points.map((point, i) => { + return [point, angles[i]]; + }); + return markers; + } + }; + + // pattern element + svg.Element.pattern = class extends svg.Element.ElementBase { + createPattern (ctx, element) { + const width = this.attribute('width').toPixels('x', true); + const height = this.attribute('height').toPixels('y', true); + + // render me using a temporary svg element + const tempSvg = new svg.Element.svg(); + tempSvg.attributes.viewBox = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes.width = new svg.Property('width', width + 'px'); + tempSvg.attributes.height = new svg.Property('height', height + 'px'); + tempSvg.attributes.transform = new svg.Property('transform', this.attribute('patternTransform').value); + tempSvg.children = this.children; + + const c = document.createElement('canvas'); + c.width = width; + c.height = height; + const cctx = c.getContext('2d'); + if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) { + cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true)); + } + // render 3x3 grid so when we transform there's no white space on edges + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + cctx.save(); + cctx.translate(x * c.width, y * c.height); + tempSvg.render(cctx); + cctx.restore(); + } + } + const pattern = ctx.createPattern(c, 'repeat'); + return pattern; + } + }; + + // marker element + svg.Element.marker = class extends svg.Element.ElementBase { + render (ctx, point, angle) { + ctx.translate(point.x, point.y); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(angle); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth); + ctx.save(); + + // render me using a temporary svg element + const tempSvg = new svg.Element.svg(); + tempSvg.attributes.viewBox = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes.refX = new svg.Property('refX', this.attribute('refX').value); + tempSvg.attributes.refY = new svg.Property('refY', this.attribute('refY').value); + tempSvg.attributes.width = new svg.Property('width', this.attribute('markerWidth').value); + tempSvg.attributes.height = new svg.Property('height', this.attribute('markerHeight').value); + tempSvg.attributes.fill = new svg.Property('fill', this.attribute('fill').valueOrDefault('black')); + tempSvg.attributes.stroke = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none')); + tempSvg.children = this.children; + tempSvg.render(ctx); + + ctx.restore(); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(-angle); + ctx.translate(-point.x, -point.y); + } + }; + + // definitions element + svg.Element.defs = class extends svg.Element.ElementBase { + render (ctx) { + // NOOP + } + }; + + // base for gradients + svg.Element.GradientBase = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.gradientUnits = this.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); + + this.stops = []; + this.children.forEach((child) => { + if (child.type === 'stop') { + this.stops.push(child); + } + }); + } + + getGradient () { + // OVERRIDE ME! + } + + createGradient (ctx, element, parentOpacityProp) { + const stopsContainer = this.getHrefAttribute().hasValue() + ? this.getHrefAttribute().getDefinition() + : this; + + const addParentOpacity = function (color) { + if (parentOpacityProp.hasValue()) { + const p = new svg.Property('color', color); + return p.addOpacity(parentOpacityProp).value; + } + return color; + }; + + const g = this.getGradient(ctx, element); + if (isNullish(g)) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color); + stopsContainer.stops.forEach(({offset, color}) => { + g.addColorStop(offset, addParentOpacity(color)); + }); + + if (this.attribute('gradientTransform').hasValue()) { + // render as transformed pattern on temporary canvas + const rootView = svg.ViewPort.viewPorts[0]; + + const rect = new svg.Element.rect(); + rect.attributes.x = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes.y = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes.width = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS); + rect.attributes.height = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS); + + const group = new svg.Element.g(); + group.attributes.transform = new svg.Property('transform', this.attribute('gradientTransform').value); + group.children = [rect]; + + const tempSvg = new svg.Element.svg(); + tempSvg.attributes.x = new svg.Property('x', 0); + tempSvg.attributes.y = new svg.Property('y', 0); + tempSvg.attributes.width = new svg.Property('width', rootView.width); + tempSvg.attributes.height = new svg.Property('height', rootView.height); + tempSvg.children = [group]; + + const c = document.createElement('canvas'); + c.width = rootView.width; + c.height = rootView.height; + const tempCtx = c.getContext('2d'); + tempCtx.fillStyle = g; + tempSvg.render(tempCtx); + return tempCtx.createPattern(c, 'no-repeat'); + } + + return g; + } + }; + + // linear gradient element + svg.Element.linearGradient = class extends svg.Element.GradientBase { + getGradient (ctx, element) { + const useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; + const bb = useBB + ? element.getBoundingBox() + : null; + + if (!this.attribute('x1').hasValue() && + !this.attribute('y1').hasValue() && + !this.attribute('x2').hasValue() && + !this.attribute('y2').hasValue() + ) { + this.attribute('x1', true).value = 0; + this.attribute('y1', true).value = 0; + this.attribute('x2', true).value = 1; + this.attribute('y2', true).value = 0; + } + + const x1 = (useBB + ? bb.x() + bb.width() * this.attribute('x1').numValue() + : this.attribute('x1').toPixels('x')); + const y1 = (useBB + ? bb.y() + bb.height() * this.attribute('y1').numValue() + : this.attribute('y1').toPixels('y')); + const x2 = (useBB + ? bb.x() + bb.width() * this.attribute('x2').numValue() + : this.attribute('x2').toPixels('x')); + const y2 = (useBB + ? bb.y() + bb.height() * this.attribute('y2').numValue() + : this.attribute('y2').toPixels('y')); + + if (x1 === x2 && y1 === y2) return null; + return ctx.createLinearGradient(x1, y1, x2, y2); + } + }; + + // radial gradient element + svg.Element.radialGradient = class extends svg.Element.GradientBase { + getGradient (ctx, element) { + const useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; + const bb = useBB ? element.getBoundingBox() : null; + + if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%'; + if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%'; + if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%'; + + const cx = (useBB + ? bb.x() + bb.width() * this.attribute('cx').numValue() + : this.attribute('cx').toPixels('x')); + const cy = (useBB + ? bb.y() + bb.height() * this.attribute('cy').numValue() + : this.attribute('cy').toPixels('y')); + + let fx = cx; + let fy = cy; + if (this.attribute('fx').hasValue()) { + fx = (useBB + ? bb.x() + bb.width() * this.attribute('fx').numValue() + : this.attribute('fx').toPixels('x')); + } + if (this.attribute('fy').hasValue()) { + fy = (useBB + ? bb.y() + bb.height() * this.attribute('fy').numValue() + : this.attribute('fy').toPixels('y')); + } + + const r = (useBB + ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue() + : this.attribute('r').toPixels()); + + return ctx.createRadialGradient(fx, fy, 0, cx, cy, r); + } + }; + + // gradient stop element + svg.Element.stop = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.offset = this.attribute('offset').numValue(); + if (this.offset < 0) this.offset = 0; + if (this.offset > 1) this.offset = 1; + + let stopColor = this.style('stop-color'); + if (this.style('stop-opacity').hasValue()) { + stopColor = stopColor.addOpacity(this.style('stop-opacity')); + } + this.color = stopColor.value; + } + }; + + // animation base element + svg.Element.AnimateBase = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + svg.Animations.push(this); + + this.duration = 0.0; + this.begin = this.attribute('begin').toMilliseconds(); + this.maxDuration = this.begin + this.attribute('dur').toMilliseconds(); + + this.initialValue = null; + this.initialUnits = ''; + this.removed = false; + + this.from = this.attribute('from'); + this.to = this.attribute('to'); + this.values = this.attribute('values'); + if (this.values.hasValue()) this.values.value = this.values.value.split(';'); + } + + getProperty () { + const attributeType = this.attribute('attributeType').value; + const attributeName = this.attribute('attributeName').value; + + if (attributeType === 'CSS') { + return this.parent.style(attributeName, true); + } + return this.parent.attribute(attributeName, true); + } + + calcValue () { + // OVERRIDE ME! + return ''; + } + + update (delta) { + // set initial value + if (isNullish(this.initialValue)) { + this.initialValue = this.getProperty().value; + this.initialUnits = this.getProperty().getUnits(); + } + + // if we're past the end time + if (this.duration > this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value === 'indefinite' || + this.attribute('repeatDur').value === 'indefinite') { + this.duration = 0.0; + } else if (this.attribute('fill').valueOrDefault('remove') === 'freeze' && !this.frozen) { + this.frozen = true; + this.parent.animationFrozen = true; + this.parent.animationFrozenValue = this.getProperty().value; + } else if (this.attribute('fill').valueOrDefault('remove') === 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue; + return true; + } + return false; + } + this.duration = this.duration + delta; + + // if we're past the begin time + let updated = false; + if (this.begin < this.duration) { + let newValue = this.calcValue(); // tween + + if (this.attribute('type').hasValue()) { + // for transform, etc. + const type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } + + this.getProperty().value = newValue; + updated = true; + } + + return updated; + } + + // fraction of duration we've covered + progress () { + const ret = {progress: (this.duration - this.begin) / (this.maxDuration - this.begin)}; + if (this.values.hasValue()) { + const p = ret.progress * (this.values.value.length - 1); + const lb = Math.floor(p), ub = Math.ceil(p); + ret.from = new svg.Property('from', parseFloat(this.values.value[lb])); + ret.to = new svg.Property('to', parseFloat(this.values.value[ub])); + ret.progress = (p - lb) / (ub - lb); + } else { + ret.from = this.from; + ret.to = this.to; + } + return ret; + } + }; + + // animate element + svg.Element.animate = class extends svg.Element.AnimateBase { + calcValue () { + const p = this.progress(); + + // tween value linearly + const newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; + return newValue + this.initialUnits; + } + }; + + // animate color element + svg.Element.animateColor = class extends svg.Element.AnimateBase { + calcValue () { + const p = this.progress(); + const from = new RGBColor(p.from.value); + const to = new RGBColor(p.to.value); + + if (from.ok && to.ok) { + // tween color linearly + const r = from.r + (to.r - from.r) * p.progress; + const g = from.g + (to.g - from.g) * p.progress; + const b = from.b + (to.b - from.b) * p.progress; + return 'rgb(' + parseInt(r) + ',' + parseInt(g) + ',' + parseInt(b) + ')'; + } + return this.attribute('from').value; + } + }; + + // animate transform element + svg.Element.animateTransform = class extends svg.Element.animate { + calcValue () { + const p = this.progress(); + + // tween value linearly + const from = svg.ToNumberArray(p.from.value); + const to = svg.ToNumberArray(p.to.value); + let newValue = ''; + from.forEach((fr, i) => { + newValue += fr + (to[i] - fr) * p.progress + ' '; + }); + return newValue; + } + }; + + // font element + svg.Element.font = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + + this.isRTL = false; + this.isArabic = false; + this.fontFace = null; + this.missingGlyph = null; + this.glyphs = []; + this.children.forEach((child) => { + if (child.type === 'font-face') { + this.fontFace = child; + if (child.style('font-family').hasValue()) { + svg.Definitions[child.style('font-family').value] = this; + } + } else if (child.type === 'missing-glyph') { + this.missingGlyph = child; + } else if (child.type === 'glyph') { + if (child.arabicForm !== '') { + this.isRTL = true; + this.isArabic = true; + if (typeof this.glyphs[child.unicode] === 'undefined') { + this.glyphs[child.unicode] = []; + } + this.glyphs[child.unicode][child.arabicForm] = child; + } else { + this.glyphs[child.unicode] = child; + } + } + }); + } + }; + + // font-face element + svg.Element.fontface = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.ascent = this.attribute('ascent').value; + this.descent = this.attribute('descent').value; + this.unitsPerEm = this.attribute('units-per-em').numValue(); + } + }; + + // missing-glyph element + svg.Element.missingglyph = class extends svg.Element.path { + constructor (node) { + super(node); + + this.horizAdvX = 0; + } + }; + + // glyph element + svg.Element.glyph = class extends svg.Element.path { + constructor (node) { + super(node); + + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + this.unicode = this.attribute('unicode').value; + this.arabicForm = this.attribute('arabic-form').value; + } + }; + + // text element + svg.Element.text = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(node, true); + } + + setContext (ctx) { + super.setContext(ctx); + + let textBaseline = this.style('dominant-baseline').toTextBaseline(); + if (isNullish(textBaseline)) textBaseline = this.style('alignment-baseline').toTextBaseline(); + if (!isNullish(textBaseline)) ctx.textBaseline = textBaseline; + } + + getBoundingBox () { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + return new svg.BoundingBox(x, y - fontSize, x + Math.floor(fontSize * 2.0 / 3.0) * this.children[0].getText().length, y); + } + + renderChildren (ctx) { + this.x = this.attribute('x').toPixels('x'); + this.y = this.attribute('y').toPixels('y'); + this.x += this.getAnchorDelta(ctx, this, 0); + this.children.forEach((child, i) => { + this.renderChild(ctx, this, i); + }); + } + + getAnchorDelta (ctx, parent, startI) { + const textAnchor = this.style('text-anchor').valueOrDefault('start'); + if (textAnchor !== 'start') { + let width = 0; + for (let i = startI; i < parent.children.length; i++) { + const child = parent.children[i]; + if (i > startI && child.attribute('x').hasValue()) break; // new group + width += child.measureTextRecursive(ctx); + } + return -1 * (textAnchor === 'end' ? width : width / 2.0); + } + return 0; + } + + renderChild (ctx, parent, i) { + const child = parent.children[i]; + if (child.attribute('x').hasValue()) { + child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i); + if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x'); + } else { + if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); + if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x'); + child.x = this.x; + } + this.x = child.x + child.measureText(ctx); + + if (child.attribute('y').hasValue()) { + child.y = child.attribute('y').toPixels('y'); + if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y'); + } else { + if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); + if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y'); + child.y = this.y; + } + this.y = child.y; + + child.render(ctx); + + for (let j = 0; j < child.children.length; j++) { + this.renderChild(ctx, child, j); + } + } + }; + + // text base + svg.Element.TextElementBase = class extends svg.Element.RenderedElementBase { + getGlyph (font, text, i) { + const c = text[i]; + let glyph = null; + if (font.isArabic) { + let arabicForm = 'isolated'; + if ((i === 0 || text[i - 1] === ' ') && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'terminal'; + if (i > 0 && text[i - 1] !== ' ' && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'medial'; + if (i > 0 && text[i - 1] !== ' ' && (i === text.length - 1 || text[i + 1] === ' ')) arabicForm = 'initial'; + if (typeof font.glyphs[c] !== 'undefined') { + glyph = font.glyphs[c][arabicForm]; + if (isNullish(glyph) && font.glyphs[c].type === 'glyph') glyph = font.glyphs[c]; + } + } else { + glyph = font.glyphs[c]; + } + if (isNullish(glyph)) glyph = font.missingGlyph; + return glyph; + } + + renderChildren (ctx) { + const customFont = this.parent.style('font-family').getDefinition(); + if (!isNullish(customFont)) { + const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + const fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); + let text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + + const dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (let i = 0; i < text.length; i++) { + const glyph = this.getGlyph(customFont, text, i); + const scale = fontSize / customFont.fontFace.unitsPerEm; + ctx.translate(this.x, this.y); + ctx.scale(scale, -scale); + const lw = ctx.lineWidth; + ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize; + if (fontStyle === 'italic') ctx.transform(1, 0, 0.4, 1, 0, 0); + glyph.render(ctx); + if (fontStyle === 'italic') ctx.transform(1, 0, -0.4, 1, 0, 0); + ctx.lineWidth = lw; + ctx.scale(1 / scale, -1 / scale); + ctx.translate(-this.x, -this.y); + + this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + this.x += dx[i]; + } + } + return; + } + + if (ctx.fillStyle !== '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); + if (ctx.strokeStyle !== '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y); + } + + getText () { + // OVERRIDE ME + } + + measureTextRecursive (ctx) { + let width = this.measureText(ctx); + this.children.forEach((child) => { + width += child.measureTextRecursive(ctx); + }); + return width; + } + + measureText (ctx) { + const customFont = this.parent.style('font-family').getDefinition(); + if (!isNullish(customFont)) { + const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + let measure = 0; + let text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + const dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (let i = 0; i < text.length; i++) { + const glyph = this.getGlyph(customFont, text, i); + measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + measure += dx[i]; + } + } + return measure; + } + + const textToMeasure = svg.compressSpaces(this.getText()); + if (!ctx.measureText) return textToMeasure.length * 10; + + ctx.save(); + this.setContext(ctx); + const {width} = ctx.measureText(textToMeasure); + ctx.restore(); + return width; + } + }; + + // tspan + svg.Element.tspan = class extends svg.Element.TextElementBase { + constructor (node) { + super(node, true); + + this.text = node.nodeValue || node.text || ''; + } + getText () { + return this.text; + } + }; + + // tref + svg.Element.tref = class extends svg.Element.TextElementBase { + getText () { + const element = this.getHrefAttribute().getDefinition(); + if (!isNullish(element)) return element.children[0].getText(); + return undefined; + } + }; + + // a element + svg.Element.a = class extends svg.Element.TextElementBase { + constructor (node) { + super(node); + + this.hasText = true; + [...node.childNodes].forEach((childNode) => { + if (childNode.nodeType !== 3) { + this.hasText = false; + } + }); + // this might contain text + this.text = this.hasText ? node.childNodes[0].nodeValue : ''; + } + + getText () { + return this.text; + } + + renderChildren (ctx) { + if (this.hasText) { + // render as text element + super.renderChildren(ctx); + const fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.toPixels('y'), this.x + this.measureText(ctx), this.y)); + } else { + // render as temporary group + const g = new svg.Element.g(); + g.children = this.children; + g.parent = this; + g.render(ctx); + } + } + + onclick () { + window.open(this.getHrefAttribute().value); + } + + onmousemove () { + svg.ctx.canvas.style.cursor = 'pointer'; + } + }; + + // image element + svg.Element.image = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(node); + + const href = this.getHrefAttribute().value; + if (href === '') { + return; + } + this._isSvg = href.match(/\.svg$/); + + svg.Images.push(this); + this.loaded = false; + if (!this._isSvg) { + this.img = document.createElement('img'); + if (svg.opts.useCORS === true) { + this.img.crossOrigin = 'Anonymous'; + } + this.img.addEventListener('load', () => { + this.loaded = true; + }); + this.img.addEventListener('error', () => { + svg.log('ERROR: image "' + href + '" not found'); + this.loaded = true; + }); + this.img.src = href; + } else { + svg.ajax(href, true).then((img) => { // eslint-disable-line promise/prefer-await-to-then, promise/always-return + this.img = img; + this.loaded = true; + }).catch((err) => { // eslint-disable-line promise/prefer-await-to-callbacks + this.erred = true; + console.error('Ajax error for canvg', err); // eslint-disable-line no-console + }); + } + } + renderChildren (ctx) { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + + const width = this.attribute('width').toPixels('x'); + const height = this.attribute('height').toPixels('y'); + if (width === 0 || height === 0) return; + + ctx.save(); + if (this._isSvg) { + ctx.drawSvg(this.img, x, y, width, height); + } else { + ctx.translate(x, y); + svg.AspectRatio( + ctx, + this.attribute('preserveAspectRatio').value, + width, + this.img.width, + height, + this.img.height, + 0, + 0 + ); + ctx.drawImage(this.img, 0, 0); + } + ctx.restore(); + } + + getBoundingBox () { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + const width = this.attribute('width').toPixels('x'); + const height = this.attribute('height').toPixels('y'); + return new svg.BoundingBox(x, y, x + width, y + height); + } + }; + + // group element + svg.Element.g = class extends svg.Element.RenderedElementBase { + getBoundingBox () { + const bb = new svg.BoundingBox(); + this.children.forEach((child) => { + bb.addBoundingBox(child.getBoundingBox()); + }); + return bb; + } + }; + + // symbol element + svg.Element.symbol = class extends svg.Element.RenderedElementBase { + render (ctx) { + // NO RENDER + } + }; + + // style element + svg.Element.style = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + // text, or spaces then CDATA + let css = ''; + [...node.childNodes].forEach(({nodeValue}) => { + css += nodeValue; + }); + // remove comments + css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // eslint-disable-line unicorn/no-unsafe-regex + // replace whitespace + css = svg.compressSpaces(css); + const cssDefs = css.split('}'); + cssDefs.forEach((cssDef) => { + if (svg.trim(cssDef) !== '') { + let [cssClasses, cssProps] = cssDef.split('{'); + cssClasses = cssClasses.split(','); + cssProps = cssProps.split(';'); + cssClasses.forEach((cssClass) => { + cssClass = svg.trim(cssClass); + if (cssClass !== '') { + const props = {}; + cssProps.forEach((cssProp) => { + const prop = cssProp.indexOf(':'); + const name = cssProp.substr(0, prop); + const value = cssProp.substr(prop + 1, cssProp.length - prop); + if (!isNullish(name) && !isNullish(value)) { + props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value)); + } + }); + svg.Styles[cssClass] = props; + if (cssClass === '@font-face') { + const fontFamily = props['font-family'].value.replace(/"/g, ''); + const srcs = props.src.value.split(','); + srcs.forEach((src) => { + if (src.includes('format("svg")')) { + const urlStart = src.indexOf('url'); + const urlEnd = src.indexOf(')', urlStart); + const url = src.substr(urlStart + 5, urlEnd - urlStart - 6); + // Can this ajax safely be converted to async? + const doc = svg.parseXml(svg.ajax(url)); + const fonts = doc.getElementsByTagName('font'); + [...fonts].forEach((font) => { + font = svg.CreateElement(font); + svg.Definitions[fontFamily] = font; + }); + } + }); + } + } + }); + } + }); + } + }; + + // use element + svg.Element.use = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(node); + + this._el = this.getHrefAttribute().getDefinition(); + } + + setContext (ctx) { + super.setContext(ctx); + if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0); + if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y')); + } + + path (ctx) { + const {_el: element} = this; + if (!isNullish(element)) element.path(ctx); + } + + getBoundingBox () { + const {_el: element} = this; + if (!isNullish(element)) return element.getBoundingBox(); + return undefined; + } + + renderChildren (ctx) { + const {_el: element} = this; + if (!isNullish(element)) { + let tempSvg = element; + if (element.type === 'symbol') { + // render me using a temporary svg element in symbol cases (https://www.w3.org/TR/SVG/struct.html#UseElement) + tempSvg = new svg.Element.svg(); + tempSvg.type = 'svg'; + tempSvg.attributes.viewBox = new svg.Property('viewBox', element.attribute('viewBox').value); + tempSvg.attributes.preserveAspectRatio = new svg.Property('preserveAspectRatio', element.attribute('preserveAspectRatio').value); + tempSvg.attributes.overflow = new svg.Property('overflow', element.attribute('overflow').value); + tempSvg.children = element.children; + } + if (tempSvg.type === 'svg') { + // if symbol or svg, inherit width/height from me + if (this.attribute('width').hasValue()) tempSvg.attributes.width = new svg.Property('width', this.attribute('width').value); + if (this.attribute('height').hasValue()) tempSvg.attributes.height = new svg.Property('height', this.attribute('height').value); + } + const oldParent = tempSvg.parent; + tempSvg.parent = null; + tempSvg.render(ctx); + tempSvg.parent = oldParent; + } + } + }; + + // mask element + svg.Element.mask = class extends svg.Element.ElementBase { + apply (ctx, element) { + // render as temp svg + let x = this.attribute('x').toPixels('x'); + let y = this.attribute('y').toPixels('y'); + let width = this.attribute('width').toPixels('x'); + let height = this.attribute('height').toPixels('y'); + + if (width === 0 && height === 0) { + const bb = new svg.BoundingBox(); + this.children.forEach((child) => { + bb.addBoundingBox(child.getBoundingBox()); + }); + x = Math.floor(bb.x1); + y = Math.floor(bb.y1); + width = Math.floor(bb.width()); + height = Math.floor(bb.height()); + } + + // temporarily remove mask to avoid recursion + const mask = element.attribute('mask').value; + element.attribute('mask').value = ''; + + const cMask = document.createElement('canvas'); + cMask.width = x + width; + cMask.height = y + height; + const maskCtx = cMask.getContext('2d'); + this.renderChildren(maskCtx); + + const c = document.createElement('canvas'); + c.width = x + width; + c.height = y + height; + const tempCtx = c.getContext('2d'); + element.render(tempCtx); + tempCtx.globalCompositeOperation = 'destination-in'; + tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat'); + tempCtx.fillRect(0, 0, x + width, y + height); + + ctx.fillStyle = tempCtx.createPattern(c, 'no-repeat'); + ctx.fillRect(0, 0, x + width, y + height); + + // reassign mask + element.attribute('mask').value = mask; + } + + render (ctx) { + // NO RENDER + } + }; + + // clip element + svg.Element.clipPath = class extends svg.Element.ElementBase { + apply (ctx) { + this.children.forEach((child) => { + if (typeof child.path !== 'undefined') { + let transform = null; + if (child.attribute('transform').hasValue()) { + transform = new svg.Transform(child.attribute('transform').value); + transform.apply(ctx); + } + child.path(ctx); + ctx.clip(); + if (transform) { transform.unapply(ctx); } + } + }); + } + render (ctx) { + // NO RENDER + } + }; + + // filters + svg.Element.filter = class extends svg.Element.ElementBase { + apply (ctx, element) { + // render as temp svg + const bb = element.getBoundingBox(); + const x = Math.floor(bb.x1); + const y = Math.floor(bb.y1); + const width = Math.floor(bb.width()); + const height = Math.floor(bb.height()); + + // temporarily remove filter to avoid recursion + const filter = element.style('filter').value; + element.style('filter').value = ''; + + let px = 0, py = 0; + this.children.forEach((child) => { + const efd = child.extraFilterDistance || 0; + px = Math.max(px, efd); + py = Math.max(py, efd); + }); + + const c = document.createElement('canvas'); + c.width = width + 2 * px; + c.height = height + 2 * py; + const tempCtx = c.getContext('2d'); + tempCtx.translate(-x + px, -y + py); + element.render(tempCtx); + + // apply filters + this.children.forEach((child) => { + child.apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py); + }); + + // render on me + ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py); + + // reassign filter + element.style('filter', true).value = filter; + } + + render (ctx) { + // NO RENDER + } + }; + + svg.Element.feMorphology = class extends svg.Element.ElementBase { + apply (ctx, x, y, width, height) { + // TODO: implement + } + }; + + svg.Element.feComposite = class extends svg.Element.ElementBase { + apply (ctx, x, y, width, height) { + // TODO: implement + } + }; + + /** + * @param {Uint8ClampedArray} img + * @param {Integer} x + * @param {Integer} y + * @param {Float} width + * @param {Float} height + * @param {Integer} rgba + * @returns {Integer} + */ + function imGet (img, x, y, width, height, rgba) { + return img[y * width * 4 + x * 4 + rgba]; + } + + /** + * @param {Uint8ClampedArray} img + * @param {Integer} x + * @param {Integer} y + * @param {Float} width + * @param {Float} height + * @param {Integer} rgba + * @param {Float} val + * @returns {void} + */ + function imSet (img, x, y, width, height, rgba, val) { + img[y * width * 4 + x * 4 + rgba] = val; + } + + svg.Element.feColorMatrix = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + let matrix = svg.ToNumberArray(this.attribute('values').value); + switch (this.attribute('type').valueOrDefault('matrix')) { // https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement + case 'saturate': { + const s = matrix[0]; + matrix = [ + 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0, + 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0, + 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1 + ]; + break; + } case 'hueRotate': { + const a = matrix[0] * Math.PI / 180.0; + const c = function (m1, m2, m3) { + return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; + }; + matrix = [ + c(0.213, 0.787, -0.213), c(0.715, -0.715, -0.715), c(0.072, -0.072, 0.928), 0, 0, + c(0.213, -0.213, 0.143), c(0.715, 0.285, 0.140), c(0.072, -0.072, -0.283), 0, 0, + c(0.213, -0.213, -0.787), c(0.715, -0.715, 0.715), c(0.072, 0.928, 0.072), 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1 + ]; + break; + } case 'luminanceToAlpha': + matrix = [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0.2125, 0.7154, 0.0721, 0, 0, + 0, 0, 0, 0, 1 + ]; + break; + } + this.matrix = matrix; + + this._m = (i, v) => { + const mi = matrix[i]; + return mi * (mi < 0 ? v - 255 : v); + }; + } + apply (ctx, x, y, width, height) { + const {_m: m} = this; + // assuming x==0 && y==0 for now + const srcData = ctx.getImageData(0, 0, width, height); + for (let _y = 0; _y < height; _y++) { + for (let _x = 0; _x < width; _x++) { + const r = imGet(srcData.data, _x, _y, width, height, 0); + const g = imGet(srcData.data, _x, _y, width, height, 1); + const b = imGet(srcData.data, _x, _y, width, height, 2); + const a = imGet(srcData.data, _x, _y, width, height, 3); + imSet(srcData.data, _x, _y, width, height, 0, m(0, r) + m(1, g) + m(2, b) + m(3, a) + m(4, 1)); + imSet(srcData.data, _x, _y, width, height, 1, m(5, r) + m(6, g) + m(7, b) + m(8, a) + m(9, 1)); + imSet(srcData.data, _x, _y, width, height, 2, m(10, r) + m(11, g) + m(12, b) + m(13, a) + m(14, 1)); + imSet(srcData.data, _x, _y, width, height, 3, m(15, r) + m(16, g) + m(17, b) + m(18, a) + m(19, 1)); + } + } + ctx.clearRect(0, 0, width, height); + ctx.putImageData(srcData, 0, 0); + } + }; + + svg.Element.feGaussianBlur = class extends svg.Element.ElementBase { + constructor (node) { + super(node); + + this.blurRadius = Math.floor(this.attribute('stdDeviation').numValue()); + this.extraFilterDistance = this.blurRadius; + } + + apply (ctx, x, y, width, height) { + // Todo: This might not be a problem anymore with out `instanceof` fix + // StackBlur requires canvas be on document + ctx.canvas.id = svg.UniqueId(); + ctx.canvas.style.display = 'none'; + document.body.append(ctx.canvas); + canvasRGBA(ctx.canvas, x, y, width, height, this.blurRadius); + ctx.canvas.remove(); + } + }; + + // title element, do nothing + svg.Element.title = class extends svg.Element.ElementBase { + constructor (node) { + super(); + } + }; + + // desc element, do nothing + svg.Element.desc = class extends svg.Element.ElementBase { + constructor (node) { + super(); + } + }; + + svg.Element.MISSING = class extends svg.Element.ElementBase { + constructor (node) { + super(); + svg.log('ERROR: Element \'' + node.nodeName + '\' not yet implemented.'); + } + }; + + // element factory + svg.CreateElement = function (node) { + const className = node.nodeName + .replace(/^[^:]+:/, '') // remove namespace + .replace(/-/g, ''); // remove dashes + let e; + if (typeof svg.Element[className] !== 'undefined') { + e = new svg.Element[className](node); + } else { + e = new svg.Element.MISSING(node); + } + + e.type = node.nodeName; + return e; + }; + + // load from url + svg.load = async function (ctx, url) { + const dom = await svg.ajax(url, true); + return svg.loadXml(ctx, dom); + }; + + // load from xml + svg.loadXml = function (ctx, xml) { + return svg.loadXmlDoc(ctx, svg.parseXml(xml)); + }; + + svg.loadXmlDoc = function (ctx, dom) { + let res; + svg.init(ctx); + + const mapXY = function (p) { + let e = ctx.canvas; + while (e) { + p.x -= e.offsetLeft; + p.y -= e.offsetTop; + e = e.offsetParent; + } + if (window.scrollX) p.x += window.scrollX; + if (window.scrollY) p.y += window.scrollY; + return p; + }; + + // bind mouse + if (svg.opts.ignoreMouse !== true) { + ctx.canvas.addEventListener('click', function (e) { + const args = !isNullish(e) + ? [e.clientX, e.clientY] + : [event.clientX, event.clientY]; // eslint-disable-line no-restricted-globals + const {x, y} = mapXY(new svg.Point(...args)); + svg.Mouse.onclick(x, y); + }); + ctx.canvas.addEventListener('mousemove', function (e) { + const args = !isNullish(e) + ? [e.clientX, e.clientY] + : [event.clientX, event.clientY]; // eslint-disable-line no-restricted-globals + const {x, y} = mapXY(new svg.Point(...args)); + svg.Mouse.onmousemove(x, y); + }); + } + + const e = svg.CreateElement(dom.documentElement); + e.root = true; + + // render loop + let isFirstRender = true; + const draw = function (resolve) { + svg.ViewPort.Clear(); + if (ctx.canvas.parentNode) { + svg.ViewPort.SetCurrent( + ctx.canvas.parentNode.clientWidth, + ctx.canvas.parentNode.clientHeight + ); + } + + if (svg.opts.ignoreDimensions !== true) { + // set canvas size + if (e.style('width').hasValue()) { + ctx.canvas.width = e.style('width').toPixels('x'); + ctx.canvas.style.width = ctx.canvas.width + 'px'; + } + if (e.style('height').hasValue()) { + ctx.canvas.height = e.style('height').toPixels('y'); + ctx.canvas.style.height = ctx.canvas.height + 'px'; + } + } + let cWidth = ctx.canvas.clientWidth || ctx.canvas.width; + let cHeight = ctx.canvas.clientHeight || ctx.canvas.height; + if (svg.opts.ignoreDimensions === true && + e.style('width').hasValue() && e.style('height').hasValue() + ) { + cWidth = e.style('width').toPixels('x'); + cHeight = e.style('height').toPixels('y'); + } + svg.ViewPort.SetCurrent(cWidth, cHeight); + + if (!isNullish(svg.opts.offsetX)) { + e.attribute('x', true).value = svg.opts.offsetX; + } + if (!isNullish(svg.opts.offsetY)) { + e.attribute('y', true).value = svg.opts.offsetY; + } + if (!isNullish(svg.opts.scaleWidth) || !isNullish(svg.opts.scaleHeight)) { + const viewBox = svg.ToNumberArray(e.attribute('viewBox').value); + let xRatio = null, yRatio = null; + + if (!isNullish(svg.opts.scaleWidth)) { + if (e.attribute('width').hasValue()) { + xRatio = e.attribute('width').toPixels('x') / svg.opts.scaleWidth; + } else if (!isNaN(viewBox[2])) { + xRatio = viewBox[2] / svg.opts.scaleWidth; + } + } + + if (!isNullish(svg.opts.scaleHeight)) { + if (e.attribute('height').hasValue()) { + yRatio = e.attribute('height').toPixels('y') / svg.opts.scaleHeight; + } else if (!isNaN(viewBox[3])) { + yRatio = viewBox[3] / svg.opts.scaleHeight; + } + } + + if (isNullish(xRatio)) { xRatio = yRatio; } + if (isNullish(yRatio)) { yRatio = xRatio; } + + e.attribute('width', true).value = svg.opts.scaleWidth; + e.attribute('height', true).value = svg.opts.scaleHeight; + e.attribute('viewBox', true).value = '0 0 ' + (cWidth * xRatio) + ' ' + (cHeight * yRatio); + e.attribute('preserveAspectRatio', true).value = 'none'; + } + + // clear and render + if (svg.opts.ignoreClear !== true) { + ctx.clearRect(0, 0, cWidth, cHeight); + } + e.render(ctx); + if (isFirstRender) { + isFirstRender = false; + resolve(dom); + } + }; + + let waitingForImages = true; + svg.intervalID = setInterval(function () { + let needUpdate = false; + + if (waitingForImages && svg.ImagesLoaded()) { + waitingForImages = false; + needUpdate = true; + } + + // need update from mouse events? + if (svg.opts.ignoreMouse !== true) { + needUpdate = needUpdate || svg.Mouse.hasEvents(); + } + + // need update from animations? + if (svg.opts.ignoreAnimation !== true) { + svg.Animations.forEach((animation) => { + const needAnimationUpdate = animation.update(1000 / svg.FRAMERATE); + needUpdate = needUpdate || needAnimationUpdate; + }); + } + + // need update from redraw? + if (typeof svg.opts.forceRedraw === 'function') { + if (svg.opts.forceRedraw() === true) { + needUpdate = true; + } + } + + // render if needed + if (needUpdate) { + draw(res); + svg.Mouse.runEvents(); // run and clear our events + } + }, 1000 / svg.FRAMERATE); + // Todo: Replace with an image loading Promise utility? + return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new + if (svg.ImagesLoaded()) { + waitingForImages = false; + draw(resolve); + return; + } + res = resolve; + }); + }; + + svg.stop = () => { + if (svg.intervalID) { + clearInterval(svg.intervalID); + } + }; + + svg.Mouse = { + events: [], + hasEvents () { return this.events.length !== 0; }, + + onclick (x, y) { + this.events.push({ + type: 'onclick', x, y, + run (e) { if (e.onclick) e.onclick(); } + }); + }, + + onmousemove (x, y) { + this.events.push({ + type: 'onmousemove', x, y, + run (e) { if (e.onmousemove) e.onmousemove(); } + }); + }, + + eventElements: [], + + checkPath (element, ctx) { + this.events.forEach(({x, y}, i) => { + if (ctx.isPointInPath && ctx.isPointInPath(x, y)) { + this.eventElements[i] = element; + } + }); + }, + + checkBoundingBox (element, bb) { + this.events.forEach(({x, y}, i) => { + if (bb.isPointInBox(x, y)) { + this.eventElements[i] = element; + } + }); + }, + + runEvents () { + svg.ctx.canvas.style.cursor = ''; + + this.events.forEach((e, i) => { + let element = this.eventElements[i]; + while (element) { + e.run(element); + element = element.parent; + } + }); + + // done running, clear + this.events = []; + this.eventElements = []; + } + }; + + return svg; +} + +if (typeof CanvasRenderingContext2D !== 'undefined') { + CanvasRenderingContext2D.prototype.drawSvg = function (s, dx, dy, dw, dh) { + canvg(this.canvas, s, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true, + ignoreClear: true, + offsetX: dx, + offsetY: dy, + scaleWidth: dw, + scaleHeight: dh + }); + }; +} diff --git a/src/image_occlusion_enhanced/svg-edit/editor/canvg/rgbcolor.js b/src/image_occlusion_enhanced/svg-edit/editor/canvg/rgbcolor.js new file mode 100644 index 00000000..e5435e2e --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/editor/canvg/rgbcolor.js @@ -0,0 +1,282 @@ +/** + * For parsing color values + * @module RGBColor + * @author Stoyan Stefanov + * @see https://www.phpied.com/rgb-color-parser-in-javascript/ + * @license MIT +*/ +const simpleColors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred: 'cd5c5c', + indigo: '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' +}; + +// array of color definition objects +const colorDefs = [ + { + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + // re: /^rgb\((?\d{1,3}),\s*(?\d{1,3}),\s*(?\d{1,3})\)$/, + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], + process (_, ...bits) { + return bits.map((b) => parseInt(b)); + } + }, + { + re: /^(\w{2})(\w{2})(\w{2})$/, + // re: /^(?\w{2})(?\w{2})(?\w{2})$/, + example: ['#00ff00', '336699'], + process (_, ...bits) { + return bits.map((b) => parseInt(b, 16)); + } + }, + { + re: /^(\w{1})(\w{1})(\w{1})$/, + // re: /^(?\w{1})(?\w{1})(?\w{1})$/, + example: ['#fb0', 'f0f'], + process (_, ...bits) { + return bits.map((b) => parseInt(b + b, 16)); + } + } +]; + +/** + * A class to parse color values. + */ +export default class RGBColor { + /** + * @param {string} colorString + */ + constructor (colorString) { + this.ok = false; + + // strip any leading # + if (colorString.charAt(0) === '#') { // remove # if any + colorString = colorString.substr(1, 6); + } + + colorString = colorString.replace(/ /g, ''); + colorString = colorString.toLowerCase(); + + // before getting into regexps, try simple matches + // and overwrite the input + if (colorString in simpleColors) { + colorString = simpleColors[colorString]; + } + // end of simple type-in colors + + // search through the definitions to find a match + + colorDefs.forEach(({re, process: processor}) => { + const bits = re.exec(colorString); + if (bits) { + const [r, g, b] = processor(...bits); + Object.assign(this, {r, g, b}); + this.ok = true; + } + }); + + // validate/cleanup values + this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); + this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); + this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); + } + + // some getters + /** + * @returns {string} + */ + toRGB () { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + } + + /** + * @returns {string} + */ + toHex () { + let r = this.r.toString(16); + let g = this.g.toString(16); + let b = this.b.toString(16); + if (r.length === 1) { r = '0' + r; } + if (g.length === 1) { g = '0' + g; } + if (b.length === 1) { b = '0' + b; } + return '#' + r + g + b; + } + + /** + * Offers a bulleted list of help. + * @returns {HTMLUListElement} + */ + static getHelpXML () { + const examples = [ + // add regexps + ...colorDefs.flatMap(({example}) => { + return example; + }), + // add type-in colors + ...Object.keys(simpleColors) + ]; + + const xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + + xml.append(...examples.map((example) => { + try { + const listItem = document.createElement('li'); + const listColor = new RGBColor(example); + const exampleDiv = document.createElement('div'); + exampleDiv.style.cssText = ` + margin: 3px; + border: 1px solid black; + background: ${listColor.toHex()}; + color: ${listColor.toHex()};`; + exampleDiv.append('test'); + const listItemValue = ` ${example} -> ${listColor.toRGB()} -> ${listColor.toHex()}`; + listItem.append(exampleDiv, listItemValue); + return listItem; + } catch (e) { + return ''; + } + })); + return xml; + } +} diff --git a/src/image_occlusion_enhanced/svg-edit/editor/contextmenu.js b/src/image_occlusion_enhanced/svg-edit/editor/contextmenu.js new file mode 100644 index 00000000..294ebbb9 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/editor/contextmenu.js @@ -0,0 +1,103 @@ +/* globals jQuery */ +/** + * Adds context menu functionality + * @module contextmenu + * @license Apache-2.0 + * @author Adam Bender + */ +// Dependencies: +// 1) jQuery (for dom injection of context menus) + +const $ = jQuery; + +let contextMenuExtensions = {}; + +/** + * Signature depends on what the user adds; in the case of our uses with + * SVGEditor, no parameters are passed nor anything expected for a return. + * @callback module:contextmenu.MenuItemAction +*/ + +/** +* @typedef {PlainObject} module:contextmenu.MenuItem +* @property {string} id +* @property {string} label +* @property {module:contextmenu.MenuItemAction} action +*/ + +/** +* @param {module:contextmenu.MenuItem} menuItem +* @returns {boolean} +*/ +const menuItemIsValid = function (menuItem) { + return menuItem && menuItem.id && menuItem.label && menuItem.action && typeof menuItem.action === 'function'; +}; + +/** +* @function module:contextmenu.add +* @param {module:contextmenu.MenuItem} menuItem +* @throws {Error|TypeError} +* @returns {void} +*/ +export const add = function (menuItem) { + // menuItem: {id, label, shortcut, action} + if (!menuItemIsValid(menuItem)) { + throw new TypeError('Menu items must be defined and have at least properties: id, label, action, where action must be a function'); + } + if (menuItem.id in contextMenuExtensions) { + throw new Error('Cannot add extension "' + menuItem.id + '", an extension by that name already exists"'); + } + // Register menuItem action, see below for deferred menu dom injection + console.log('Registered contextmenu item: {id:' + menuItem.id + ', label:' + menuItem.label + '}'); // eslint-disable-line no-console + contextMenuExtensions[menuItem.id] = menuItem; + // TODO: Need to consider how to handle custom enable/disable behavior +}; + +/** +* @function module:contextmenu.hasCustomHandler +* @param {string} handlerKey +* @returns {boolean} +*/ +export const hasCustomHandler = function (handlerKey) { + return Boolean(contextMenuExtensions[handlerKey]); +}; + +/** +* @function module:contextmenu.getCustomHandler +* @param {string} handlerKey +* @returns {module:contextmenu.MenuItemAction} +*/ +export const getCustomHandler = function (handlerKey) { + return contextMenuExtensions[handlerKey].action; +}; + +/** +* @param {module:contextmenu.MenuItem} menuItem +* @returns {void} +*/ +const injectExtendedContextMenuItemIntoDom = function (menuItem) { + if (!Object.keys(contextMenuExtensions).length) { + // all menuItems appear at the bottom of the menu in their own container. + // if this is the first extension menu we need to add the separator. + $('#cmenu_canvas').append("
      • "); + } + const shortcut = menuItem.shortcut || ''; + $('#cmenu_canvas').append("
      • " + + menuItem.label + "" + + shortcut + '
      • '); +}; + +/** +* @function module:contextmenu.injectExtendedContextMenuItemsIntoDom +* @returns {void} +*/ +export const injectExtendedContextMenuItemsIntoDom = function () { + Object.values(contextMenuExtensions).forEach((menuItem) => { + injectExtendedContextMenuItemIntoDom(menuItem); + }); +}; +/** +* @function module:contextmenu.resetCustomMenus +* @returns {void} +*/ +export const resetCustomMenus = function () { contextMenuExtensions = {}; }; diff --git a/src/image_occlusion_enhanced/svg-edit/editor/contextmenu/jQuery.contextMenu.js b/src/image_occlusion_enhanced/svg-edit/editor/contextmenu/jQuery.contextMenu.js new file mode 100755 index 00000000..e393b844 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/editor/contextmenu/jQuery.contextMenu.js @@ -0,0 +1,266 @@ +/** + * jQuery Context Menu Plugin + * Cory S.N. LaViska + * A Beautiful Site ({@link https://abeautifulsite.net/}) + * Modified by Alexis Deveria + * + * More info: {@link https://abeautifulsite.net/2008/09/jquery-context-menu-plugin/} + * + * @module jQueryContextMenu + * @todo Update to latest version and adapt (and needs jQuery update as well): {@link https://github.com/swisnl/jQuery-contextMenu} + * @version 1.0.1 + * + * @license (GPL-2.0-or-later OR MIT) + * + * This plugin is dual-licensed under the GNU General Public License + * and the MIT License and is copyright A Beautiful Site, LLC. + * +*/ +import {isMac} from '../browser.js'; + +/** +* @callback module:jQueryContextMenu.jQueryContextMenuListener +* @param {string} href The `href` value after the first character (for bypassing an initial `#`) +* @param {external:jQuery} srcElement The wrapped jQuery srcElement +* @param {{x: Float, y: Float, docX: Float, docY: Float}} coords +*/ + +/** +* @typedef {PlainObject} module:jQueryContextMenu.jQueryContextMenuConfig +* @property {string} menu +* @property {Float} inSpeed +* @property {Float} outSpeed +* @property {boolean} allowLeft +*/ + +/** +* Adds {@link external:jQuery.fn.contextMenu}, +* {@link external:jQuery.fn.disableContextMenuItems}, +* {@link external:jQuery.fn.enableContextMenuItems}, +* {@link external:jQuery.fn.disableContextMenu}, +* {@link external:jQuery.fn.enableContextMenu}, +* {@link external:jQuery.fn.destroyContextMenu}. +* @function module:jQueryContextMenu.jQueryContextMenu +* @param {external:jQuery} $ The jQuery object to wrap (with `contextMenu`, `disableContextMenuItems`, `enableContextMenuItems`, `disableContextMenu`, `enableContextMenu`, `destroyContextMenu`) +* @returns {external:jQuery} +*/ +function jQueryContextMenu ($) { + const win = $(window); + const doc = $(document); + + $.extend($.fn, { + /** + * @memberof external:jQuery.fn + * @param {module:jQueryContextMenu.jQueryContextMenuConfig} o + * @param {module:jQueryContextMenu.jQueryContextMenuListener} listener + * @returns {external:jQuery} + */ + contextMenu (o, listener) { + // Defaults + if (o.menu === undefined) return false; + if (o.inSpeed === undefined) o.inSpeed = 150; + if (o.outSpeed === undefined) o.outSpeed = 75; + // 0 needs to be -1 for expected results (no fade) + if (o.inSpeed === 0) o.inSpeed = -1; + if (o.outSpeed === 0) o.outSpeed = -1; + // Loop each context menu + $(this).each(function () { + const el = $(this); + const offset = $(el).offset(); + + const menu = $('#' + o.menu); + + // Add contextMenu class + menu.addClass('contextMenu'); + // Simulate a true right click + $(this).bind('mousedown', function (evt) { + $(this).mouseup(function (e) { + const srcElement = $(this); + srcElement.unbind('mouseup'); + + if (!(evt.button === 2 || o.allowLeft || + (evt.ctrlKey && isMac()))) { + return undefined; + } + e.stopPropagation(); + // Hide context menus that may be showing + $('.contextMenu').hide(); + // Get this context menu + + if (el.hasClass('disabled')) return false; + + // Detect mouse position + let x = e.pageX, y = e.pageY; + + const xOff = win.width() - menu.width(), + yOff = win.height() - menu.height(); + + if (x > xOff - 15) x = xOff - 15; + if (y > yOff - 30) y = yOff - 30; // 30 is needed to prevent scrollbars in FF + + // Show the menu + doc.unbind('click'); + menu.css({top: y, left: x}).fadeIn(o.inSpeed); + // Hover events + menu.find('A').mouseover(function () { + menu.find('LI.hover').removeClass('hover'); + $(this).parent().addClass('hover'); + }).mouseout(function () { + menu.find('LI.hover').removeClass('hover'); + }); + + // Keyboard + doc.keypress(function (ev) { + switch (ev.keyCode) { + case 38: // up + if (!menu.find('LI.hover').length) { + menu.find('LI:last').addClass('hover'); + } else { + menu.find('LI.hover').removeClass('hover').prevAll('LI:not(.disabled)').eq(0).addClass('hover'); + if (!menu.find('LI.hover').length) menu.find('LI:last').addClass('hover'); + } + break; + case 40: // down + if (!menu.find('LI.hover').length) { + menu.find('LI:first').addClass('hover'); + } else { + menu.find('LI.hover').removeClass('hover').nextAll('LI:not(.disabled)').eq(0).addClass('hover'); + if (!menu.find('LI.hover').length) menu.find('LI:first').addClass('hover'); + } + break; + case 13: // enter + menu.find('LI.hover A').trigger('click'); + break; + case 27: // esc + doc.trigger('click'); + break; + } + }); + + // When items are selected + menu.find('A').unbind('mouseup'); + menu.find('LI:not(.disabled) A').mouseup(function () { + doc.unbind('click').unbind('keypress'); + $('.contextMenu').hide(); + if (listener) { + listener($(this).attr('href').substr(1), $(srcElement), { + x: x - offset.left, y: y - offset.top, docX: x, docY: y + }); + } + return false; + }); + + // Hide bindings + setTimeout(function () { // Delay for Mozilla + doc.click(function () { + doc.unbind('click').unbind('keypress'); + menu.fadeOut(o.outSpeed); + return false; + }); + }, 0); + return undefined; + }); + }); + + // Disable text selection + if ($.browser.mozilla) { + $('#' + o.menu).each(function () { $(this).css({MozUserSelect: 'none'}); }); + } else if ($.browser.msie) { + $('#' + o.menu).each(function () { $(this).bind('selectstart.disableTextSelect', function () { return false; }); }); + } else { + $('#' + o.menu).each(function () { $(this).bind('mousedown.disableTextSelect', function () { return false; }); }); + } + // Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome) + $(el).add($('UL.contextMenu')).bind('contextmenu', function () { return false; }); + }); + return $(this); + }, + + /** + * Disable context menu items on the fly. + * @memberof external:jQuery.fn + * @param {void|string} o Comma-separated + * @returns {external:jQuery} + */ + disableContextMenuItems (o) { + if (o === undefined) { + // Disable all + $(this).find('LI').addClass('disabled'); + return $(this); + } + $(this).each(function () { + if (o !== undefined) { + const d = o.split(','); + for (const href of d) { + $(this).find('A[href="' + href + '"]').parent().addClass('disabled'); + } + } + }); + return $(this); + }, + + /** + * Enable context menu items on the fly. + * @memberof external:jQuery.fn + * @param {void|string} o Comma-separated + * @returns {external:jQuery} + */ + enableContextMenuItems (o) { + if (o === undefined) { + // Enable all + $(this).find('LI.disabled').removeClass('disabled'); + return $(this); + } + $(this).each(function () { + if (o !== undefined) { + const d = o.split(','); + for (const href of d) { + $(this).find('A[href="' + href + '"]').parent().removeClass('disabled'); + } + } + }); + return $(this); + }, + + /** + * Disable context menu(s). + * @memberof external:jQuery.fn + * @returns {external:jQuery} + */ + disableContextMenu () { + $(this).each(function () { + $(this).addClass('disabled'); + }); + return $(this); + }, + + /** + * Enable context menu(s). + * @memberof external:jQuery.fn + * @returns {external:jQuery} + */ + enableContextMenu () { + $(this).each(function () { + $(this).removeClass('disabled'); + }); + return $(this); + }, + + /** + * Destroy context menu(s). + * @memberof external:jQuery.fn + * @returns {external:jQuery} + */ + destroyContextMenu () { + // Destroy specified context menus + $(this).each(function () { + // Disable action + $(this).unbind('mousedown').unbind('mouseup'); + }); + return $(this); + } + }); + return $; +} + +export default jQueryContextMenu; diff --git a/src/image_occlusion_enhanced/svg-edit/editor/coords.js b/src/image_occlusion_enhanced/svg-edit/editor/coords.js new file mode 100644 index 00000000..3b2775fb --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/editor/coords.js @@ -0,0 +1,316 @@ +/* globals jQuery */ +/** + * Manipulating coordinates + * @module coords + * @license MIT + */ + +import './svgpathseg.js'; +import { + snapToGrid, assignAttributes, getBBox, getRefElem, findDefs +} from './utilities.js'; +import { + transformPoint, transformListToTransform, matrixMultiply, transformBox +} from './math.js'; +import {getTransformList} from './svgtransformlist.js'; + +const $ = jQuery; + +// this is how we map paths to our preferred relative segment types +const pathMap = [ + 0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a', + 'H', 'h', 'V', 'v', 'S', 's', 'T', 't' +]; + +/** + * @interface module:coords.EditorContext + */ +/** + * @function module:coords.EditorContext#getGridSnapping + * @returns {boolean} + */ +/** + * @function module:coords.EditorContext#getDrawing + * @returns {module:draw.Drawing} +*/ +/** + * @function module:coords.EditorContext#getSVGRoot + * @returns {SVGSVGElement} +*/ + +let editorContext_ = null; + +/** +* @function module:coords.init +* @param {module:svgcanvas.SvgCanvas#event:pointsAdded} editorContext +* @returns {void} +*/ +export const init = function (editorContext) { + editorContext_ = editorContext; +}; + +/** + * Applies coordinate changes to an element based on the given matrix. + * @name module:coords.remapElement + * @type {module:path.EditorContext#remapElement} +*/ +export const remapElement = function (selected, changes, m) { + const remap = function (x, y) { return transformPoint(x, y, m); }, + scalew = function (w) { return m.a * w; }, + scaleh = function (h) { return m.d * h; }, + doSnapping = editorContext_.getGridSnapping() && selected.parentNode.parentNode.localName === 'svg', + finishUp = function () { + if (doSnapping) { + Object.entries(changes).forEach(([o, value]) => { + changes[o] = snapToGrid(value); + }); + } + assignAttributes(selected, changes, 1000, true); + }, + box = getBBox(selected); + + for (let i = 0; i < 2; i++) { + const type = i === 0 ? 'fill' : 'stroke'; + const attrVal = selected.getAttribute(type); + if (attrVal && attrVal.startsWith('url(')) { + if (m.a < 0 || m.d < 0) { + const grad = getRefElem(attrVal); + const newgrad = grad.cloneNode(true); + if (m.a < 0) { + // flip x + const x1 = newgrad.getAttribute('x1'); + const x2 = newgrad.getAttribute('x2'); + newgrad.setAttribute('x1', -(x1 - 1)); + newgrad.setAttribute('x2', -(x2 - 1)); + } + + if (m.d < 0) { + // flip y + const y1 = newgrad.getAttribute('y1'); + const y2 = newgrad.getAttribute('y2'); + newgrad.setAttribute('y1', -(y1 - 1)); + newgrad.setAttribute('y2', -(y2 - 1)); + } + newgrad.id = editorContext_.getDrawing().getNextId(); + findDefs().append(newgrad); + selected.setAttribute(type, 'url(#' + newgrad.id + ')'); + } + + // Not really working :( + // if (selected.tagName === 'path') { + // reorientGrads(selected, m); + // } + } + } + + const elName = selected.tagName; + if (elName === 'g' || elName === 'text' || elName === 'tspan' || elName === 'use') { + // if it was a translate, then just update x,y + if (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && (m.e !== 0 || m.f !== 0)) { + // [T][M] = [M][T'] + // therefore [T'] = [M_inv][T][M] + const existing = transformListToTransform(selected).matrix, + tNew = matrixMultiply(existing.inverse(), m, existing); + changes.x = parseFloat(changes.x) + tNew.e; + changes.y = parseFloat(changes.y) + tNew.f; + } else { + // we just absorb all matrices into the element and don't do any remapping + const chlist = getTransformList(selected); + const mt = editorContext_.getSVGRoot().createSVGTransform(); + mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m)); + chlist.clear(); + chlist.appendItem(mt); + } + } + + // now we have a set of changes and an applied reduced transform list + // we apply the changes directly to the DOM + switch (elName) { + case 'foreignObject': + case 'rect': + case 'image': { + // Allow images to be inverted (give them matrix when flipped) + if (elName === 'image' && (m.a < 0 || m.d < 0)) { + // Convert to matrix + const chlist = getTransformList(selected); + const mt = editorContext_.getSVGRoot().createSVGTransform(); + mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m)); + chlist.clear(); + chlist.appendItem(mt); + } else { + const pt1 = remap(changes.x, changes.y); + changes.width = scalew(changes.width); + changes.height = scaleh(changes.height); + changes.x = pt1.x + Math.min(0, changes.width); + changes.y = pt1.y + Math.min(0, changes.height); + changes.width = Math.abs(changes.width); + changes.height = Math.abs(changes.height); + } + finishUp(); + break; + } case 'ellipse': { + const c = remap(changes.cx, changes.cy); + changes.cx = c.x; + changes.cy = c.y; + changes.rx = scalew(changes.rx); + changes.ry = scaleh(changes.ry); + changes.rx = Math.abs(changes.rx); + changes.ry = Math.abs(changes.ry); + finishUp(); + break; + } case 'circle': { + const c = remap(changes.cx, changes.cy); + changes.cx = c.x; + changes.cy = c.y; + // take the minimum of the new selected box's dimensions for the new circle radius + const tbox = transformBox(box.x, box.y, box.width, box.height, m); + const w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y; + changes.r = Math.min(w / 2, h / 2); + + if (changes.r) { changes.r = Math.abs(changes.r); } + finishUp(); + break; + } case 'line': { + const pt1 = remap(changes.x1, changes.y1); + const pt2 = remap(changes.x2, changes.y2); + changes.x1 = pt1.x; + changes.y1 = pt1.y; + changes.x2 = pt2.x; + changes.y2 = pt2.y; + } // Fallthrough + case 'text': + case 'tspan': + case 'use': { + finishUp(); + break; + } case 'g': { + const gsvg = $(selected).data('gsvg'); + if (gsvg) { + assignAttributes(gsvg, changes, 1000, true); + } + break; + } case 'polyline': + case 'polygon': { + const len = changes.points.length; + for (let i = 0; i < len; ++i) { + const pt = changes.points[i]; + const {x, y} = remap(pt.x, pt.y); + changes.points[i].x = x; + changes.points[i].y = y; + } + + // const len = changes.points.length; + let pstr = ''; + for (let i = 0; i < len; ++i) { + const pt = changes.points[i]; + pstr += pt.x + ',' + pt.y + ' '; + } + selected.setAttribute('points', pstr); + break; + } case 'path': { + const segList = selected.pathSegList; + let len = segList.numberOfItems; + changes.d = []; + for (let i = 0; i < len; ++i) { + const seg = segList.getItem(i); + changes.d[i] = { + type: seg.pathSegType, + x: seg.x, + y: seg.y, + x1: seg.x1, + y1: seg.y1, + x2: seg.x2, + y2: seg.y2, + r1: seg.r1, + r2: seg.r2, + angle: seg.angle, + largeArcFlag: seg.largeArcFlag, + sweepFlag: seg.sweepFlag + }; + } + + len = changes.d.length; + const firstseg = changes.d[0], + currentpt = remap(firstseg.x, firstseg.y); + changes.d[0].x = currentpt.x; + changes.d[0].y = currentpt.y; + for (let i = 1; i < len; ++i) { + const seg = changes.d[i]; + const {type} = seg; + // if absolute or first segment, we want to remap x, y, x1, y1, x2, y2 + // if relative, we want to scalew, scaleh + if (type % 2 === 0) { // absolute + const thisx = (seg.x !== undefined) ? seg.x : currentpt.x, // for V commands + thisy = (seg.y !== undefined) ? seg.y : currentpt.y; // for H commands + const pt = remap(thisx, thisy); + const pt1 = remap(seg.x1, seg.y1); + const pt2 = remap(seg.x2, seg.y2); + seg.x = pt.x; + seg.y = pt.y; + seg.x1 = pt1.x; + seg.y1 = pt1.y; + seg.x2 = pt2.x; + seg.y2 = pt2.y; + seg.r1 = scalew(seg.r1); + seg.r2 = scaleh(seg.r2); + } else { // relative + seg.x = scalew(seg.x); + seg.y = scaleh(seg.y); + seg.x1 = scalew(seg.x1); + seg.y1 = scaleh(seg.y1); + seg.x2 = scalew(seg.x2); + seg.y2 = scaleh(seg.y2); + seg.r1 = scalew(seg.r1); + seg.r2 = scaleh(seg.r2); + } + } // for each segment + + let dstr = ''; + len = changes.d.length; + for (let i = 0; i < len; ++i) { + const seg = changes.d[i]; + const {type} = seg; + dstr += pathMap[type]; + switch (type) { + case 13: // relative horizontal line (h) + case 12: // absolute horizontal line (H) + dstr += seg.x + ' '; + break; + case 15: // relative vertical line (v) + case 14: // absolute vertical line (V) + dstr += seg.y + ' '; + break; + case 3: // relative move (m) + case 5: // relative line (l) + case 19: // relative smooth quad (t) + case 2: // absolute move (M) + case 4: // absolute line (L) + case 18: // absolute smooth quad (T) + dstr += seg.x + ',' + seg.y + ' '; + break; + case 7: // relative cubic (c) + case 6: // absolute cubic (C) + dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x2 + ',' + seg.y2 + ' ' + + seg.x + ',' + seg.y + ' '; + break; + case 9: // relative quad (q) + case 8: // absolute quad (Q) + dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x + ',' + seg.y + ' '; + break; + case 11: // relative elliptical arc (a) + case 10: // absolute elliptical arc (A) + dstr += seg.r1 + ',' + seg.r2 + ' ' + seg.angle + ' ' + Number(seg.largeArcFlag) + + ' ' + Number(seg.sweepFlag) + ' ' + seg.x + ',' + seg.y + ' '; + break; + case 17: // relative smooth cubic (s) + case 16: // absolute smooth cubic (S) + dstr += seg.x2 + ',' + seg.y2 + ' ' + seg.x + ',' + seg.y + ' '; + break; + } + } + + selected.setAttribute('d', dstr); + break; + } + } +}; diff --git a/src/image_occlusion_enhanced/svg-edit/editor/dbox.js b/src/image_occlusion_enhanced/svg-edit/editor/dbox.js new file mode 100644 index 00000000..1e177249 --- /dev/null +++ b/src/image_occlusion_enhanced/svg-edit/editor/dbox.js @@ -0,0 +1,175 @@ +/** + * @module jQueryPluginDBox + */ +/** +* @param {external:jQuery} $ +* @param {PlainObject} [strings] +* @param {PlainObject} [strings.ok] +* @param {PlainObject} [strings.cancel] +* @returns {external:jQuery} +*/ +export default function jQueryPluginDBox ($, strings = {ok: 'Ok', cancel: 'Cancel'}) { + // This sets up alternative dialog boxes. They mostly work the same way as + // their UI counterparts, expect instead of returning the result, a callback + // needs to be included that returns the result as its first parameter. + // In the future we may want to add additional types of dialog boxes, since + // they should be easy to handle this way. + $('#dialog_container').draggable({ + cancel: '#dialog_content, #dialog_buttons *', + containment: 'window' + }).css('position', 'absolute'); + + const box = $('#dialog_box'), + btnHolder = $('#dialog_buttons'), + dialogContent = $('#dialog_content'); + + /** + * @typedef {PlainObject} module:jQueryPluginDBox.PromiseResultObject + * @property {string|true} response + * @property {boolean} checked + */ + + /** + * Resolves to `false` (if cancelled), for prompts and selects + * without checkboxes, it resolves to the value of the form control. For other + * types without checkboxes, it resolves to `true`. For checkboxes, it resolves + * to an object with the `response` key containing the same value as the previous + * mentioned (string or `true`) and a `checked` (boolean) property. + * @typedef {Promise} module:jQueryPluginDBox.ResultPromise + */ + /** + * @typedef {PlainObject} module:jQueryPluginDBox.SelectOption + * @property {string} text + * @property {string} value + */ + /** + * @typedef {PlainObject} module:jQueryPluginDBox.CheckboxInfo + * @property {string} label Label for the checkbox + * @property {string} value Value of the checkbox + * @property {string} tooltip Tooltip on the checkbox label + * @property {boolean} checked Whether the checkbox is checked by default + */ + /** + * Triggered upon a change of value for the select pull-down. + * @callback module:jQueryPluginDBox.SelectChangeListener + * @returns {void} + */ + /** + * Creates a dialog of the specified type with a given message + * and any defaults and type-specific metadata. Returns a `Promise` + * which resolves differently depending on whether the dialog + * was cancelled or okayed (with the response and any checked state). + * @param {"alert"|"prompt"|"select"|"process"} type + * @param {string} msg + * @param {string} [defaultVal] + * @param {module:jQueryPluginDBox.SelectOption[]} [opts] + * @param {module:jQueryPluginDBox.SelectChangeListener} [changeListener] + * @param {module:jQueryPluginDBox.CheckboxInfo} [checkbox] + * @returns {jQueryPluginDBox.ResultPromise} + */ + function dbox (type, msg, defaultVal, opts, changeListener, checkbox) { + dialogContent.html('

        ' + msg.replace(/\n/g, '

        ') + '

        ') + .toggleClass('prompt', (type === 'prompt')); + btnHolder.empty(); + + const ok = $('').appendTo(btnHolder); + + return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new + if (type !== 'alert') { + $('') + .appendTo(btnHolder) + .click(function () { + box.hide(); + resolve(false); + }); + } + + let ctrl, chkbx; + if (type === 'prompt') { + ctrl = $('').prependTo(btnHolder); + ctrl.val(defaultVal || ''); + ctrl.bind('keydown', 'return', function () { ok.click(); }); + } else if (type === 'select') { + const div = $('
        '); + ctrl = $(`').appendTo(label); + chkbx.val(checkbox.value); + if (checkbox.tooltip) { + label.attr('title', checkbox.tooltip); + } + chkbx.prop('checked', Boolean(checkbox.checked)); + div.append($('
        ').append(label)); + } + $.each(opts || [], function (opt, val) { + if (typeof val === 'object') { + ctrl.append($('