summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml20
-rw-r--r--CHANGELOG.md62
-rw-r--r--Godeps/Godeps.json17
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/Makefile15
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/affine.go174
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur.go68
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur_test.go207
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/Makefile11
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve.go274
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve_test.go78
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/Makefile15
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect.go133
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect_test.go77
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/doc.go31
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral.go93
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral_test.go156
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser.go125
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser_test.go75
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector.go55
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector_test.go49
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/Makefile11
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/graphicstest.go112
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/Makefile13
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear.go206
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear_test.go143
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/doc.go25
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/interp.go29
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate.go35
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate_test.go169
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale.go31
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale_test.go153
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/shared_test.go69
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail.go41
-rw-r--r--Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail_test.go53
-rw-r--r--Godeps/_workspace/src/github.com/disintegration/imaging/LICENSE21
-rw-r--r--Godeps/_workspace/src/github.com/disintegration/imaging/README.md160
-rw-r--r--Godeps/_workspace/src/github.com/disintegration/imaging/adjust.go200
-rw-r--r--Godeps/_workspace/src/github.com/disintegration/imaging/effects.go187
-rw-r--r--Godeps/_workspace/src/github.com/disintegration/imaging/helpers.go392
-rw-r--r--Godeps/_workspace/src/github.com/disintegration/imaging/resize.go564
-rw-r--r--Godeps/_workspace/src/github.com/disintegration/imaging/tools.go182
-rw-r--r--Godeps/_workspace/src/github.com/disintegration/imaging/transform.go201
-rw-r--r--Godeps/_workspace/src/github.com/disintegration/imaging/utils.go77
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/.travis.yml7
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/LICENSE13
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/README.md149
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/converter.go452
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/converter_test.go43
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/filters.go143
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/nearest.go318
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/nearest_test.go57
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/resize.go614
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/resize_test.go314
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/thumbnail.go55
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/thumbnail_test.go47
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/ycc.go227
-rw-r--r--Godeps/_workspace/src/github.com/nfnt/resize/ycc_test.go214
-rw-r--r--Godeps/_workspace/src/golang.org/x/image/tiff/buffer.go69
-rw-r--r--Godeps/_workspace/src/golang.org/x/image/tiff/compress.go58
-rw-r--r--Godeps/_workspace/src/golang.org/x/image/tiff/consts.go133
-rw-r--r--Godeps/_workspace/src/golang.org/x/image/tiff/lzw/reader.go277
-rw-r--r--Godeps/_workspace/src/golang.org/x/image/tiff/reader.go681
-rw-r--r--Godeps/_workspace/src/golang.org/x/image/tiff/writer.go438
-rw-r--r--Makefile15
-rw-r--r--api/admin.go35
-rw-r--r--api/channel.go58
-rw-r--r--api/channel_benchmark_test.go11
-rw-r--r--api/channel_test.go80
-rw-r--r--api/command.go2
-rw-r--r--api/command_test.go8
-rw-r--r--api/context.go24
-rw-r--r--api/file.go67
-rw-r--r--api/post.go2
-rw-r--r--api/team.go5
-rw-r--r--api/user.go62
-rw-r--r--config/config.json9
-rw-r--r--doc/README.md33
-rw-r--r--doc/developer/Code-Contribution-Guidelines.md (renamed from doc/developer/code-contribution.md)2
-rw-r--r--doc/developer/Setup.md (renamed from doc/install/dev-setup.md)11
-rw-r--r--doc/developer/Style-Guide.md (renamed from doc/developer/style-guide.md)0
-rw-r--r--doc/favicon.ico0
-rw-r--r--doc/install/Amazon-Elastic-Beanstalk.md (renamed from doc/install/aws-ebs-setup.md)6
-rw-r--r--doc/install/Docker-Single-Container.md (renamed from doc/install/single-container-install.md)12
-rw-r--r--doc/install/Production-Ubuntu.md (renamed from doc/install/prod-ubuntu.md)17
-rw-r--r--doc/install/Release-Numbering.md (renamed from doc/install/release-numbering.md)0
-rw-r--r--doc/install/Requirements.md (renamed from doc/install/requirements.md)0
-rw-r--r--doc/install/SMTP-Email-Setup.md (renamed from doc/config/smtp-email-setup.md)0
-rw-r--r--doc/integrations/Single-Sign-On/Gitlab.md (renamed from doc/integrations/sso/gitlab-sso.md)0
-rw-r--r--doc/integrations/webhooks/Incoming.md62
-rw-r--r--doc/usage/Markdown.md (renamed from doc/help/enduser/markdown.md)0
-rw-r--r--doc/usage/Slack-Import.md18
-rw-r--r--docker/dev/config_docker.json9
-rw-r--r--docker/local/config_docker.json9
-rw-r--r--mattermost.go53
-rw-r--r--mkdocs.yml11
-rw-r--r--model/channel_member.go58
-rw-r--r--model/channel_member_test.go18
-rw-r--r--model/client.go4
-rw-r--r--model/config.go107
-rw-r--r--model/post.go5
-rw-r--r--model/post_test.go13
-rw-r--r--model/session.go1
-rw-r--r--model/utils.go1
-rw-r--r--model/version.go4
-rw-r--r--store/sql_channel_store.go106
-rw-r--r--store/sql_channel_store_test.go32
-rw-r--r--store/sql_post_store.go4
-rw-r--r--store/sql_post_store_test.go9
-rw-r--r--store/sql_store.go22
-rw-r--r--store/store.go2
-rw-r--r--utils/config.go7
-rw-r--r--utils/diagnostic.go45
-rw-r--r--utils/mail.go23
-rw-r--r--web/react/components/admin_console/email_settings.jsx4
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx67
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx34
-rw-r--r--web/react/components/admin_console/service_settings.jsx70
-rw-r--r--web/react/components/admin_console/sql_settings.jsx6
-rw-r--r--web/react/components/channel_notifications.jsx289
-rw-r--r--web/react/components/command_list.jsx2
-rw-r--r--web/react/components/more_direct_channels.jsx2
-rw-r--r--web/react/components/navbar_dropdown.jsx14
-rw-r--r--web/react/components/notify_counts.jsx2
-rw-r--r--web/react/components/popover_list_members.jsx26
-rw-r--r--web/react/components/post.jsx9
-rw-r--r--web/react/components/post_body.jsx122
-rw-r--r--web/react/components/post_header.jsx20
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/post_list.jsx29
-rw-r--r--web/react/components/rename_channel_modal.jsx1
-rw-r--r--web/react/components/search_results_item.jsx7
-rw-r--r--web/react/components/setting_upload.jsx45
-rw-r--r--web/react/components/sidebar.jsx35
-rw-r--r--web/react/components/sidebar_header.jsx3
-rw-r--r--web/react/components/sidebar_right.jsx25
-rw-r--r--web/react/components/sidebar_right_menu.jsx15
-rw-r--r--web/react/components/team_signup_url_page.jsx4
-rw-r--r--web/react/components/team_signup_with_sso.jsx10
-rw-r--r--web/react/components/user_profile.jsx16
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx2
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx18
-rw-r--r--web/react/components/user_settings/premade_theme_chooser.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx4
-rw-r--r--web/react/pages/authorize.jsx16
-rw-r--r--web/react/pages/channel.jsx4
-rw-r--r--web/react/utils/async_client.jsx16
-rw-r--r--web/react/utils/client.jsx20
-rw-r--r--web/react/utils/constants.jsx3
-rw-r--r--web/react/utils/text_formatting.jsx6
-rw-r--r--web/react/utils/utils.jsx229
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss1
-rw-r--r--web/sass-files/sass/partials/_base.scss28
-rw-r--r--web/sass-files/sass/partials/_command-box.scss17
-rw-r--r--web/sass-files/sass/partials/_headers.scss18
-rw-r--r--web/sass-files/sass/partials/_mentions.scss8
-rw-r--r--web/sass-files/sass/partials/_modal.scss17
-rw-r--r--web/sass-files/sass/partials/_oauth.scss31
-rw-r--r--web/sass-files/sass/partials/_post.scss8
-rw-r--r--web/sass-files/sass/partials/_responsive.scss6
-rw-r--r--web/sass-files/sass/partials/_settings.scss11
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss13
-rw-r--r--web/sass-files/sass/partials/_sidebar--menu.scss2
-rw-r--r--web/sass-files/sass/styles.scss3
-rw-r--r--web/static/images/themes/mattermost dark.pngbin75371 -> 104882 bytes
-rw-r--r--web/static/images/themes/mattermost.pngbin66828 -> 93862 bytes
-rw-r--r--web/static/images/themes/organization.pngbin86044 -> 127558 bytes
-rw-r--r--web/static/images/themes/slack.pngbin68603 -> 0 bytes
-rw-r--r--web/static/images/themes/windows dark.pngbin82784 -> 122145 bytes
-rw-r--r--web/templates/authorize.html40
-rw-r--r--web/templates/head.html19
-rw-r--r--web/web.go88
172 files changed, 5459 insertions, 6232 deletions
diff --git a/.travis.yml b/.travis.yml
index 02e1234d3..1af940d21 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,6 +20,7 @@ before_install:
- sudo sed -i'' 's/log_slow_queries/slow_query_log/' /etc/mysql/my.cnf
- sudo sed -i'' 's/basedir[^=]\+=.*$/basedir = \/opt\/mysql\/server-5.6/' /etc/mysql/my.cnf
- sudo /etc/init.d/mysql.server start
+- sudo pip install mkdocs
install:
- export PATH=$PATH:$HOME/gopath/bin
- go get github.com/tools/godep
@@ -37,6 +38,7 @@ addons:
hosts:
- 127.0.0.1 dockerhost
deploy:
+# Github releases, builds only on tags
- provider: releases
api_key:
secure: ma8Y0oimU+LB6LTAh8to2E1/ghaDPhcsAFXBrODsHpd4JgxA6HYoEwSEBCJFHSpu/JteclsxSTfp9hcuzw/IOtlwlSAiVoBZ60s24MRKTIAQNtrJ4QrX5wyfAZi+Bcuk/E8NynmoIW5qpaElSAdjgocyjAJIQ5ChMEztglL0cAEBXQRWbWMqSZ0hVLPrKDCIkWIyv3pFxqdLOxktkzxW07r2dlT0hppXR3dCaPJo0nelArS2H3LdN/3Iv6cAddfS27RaZkqDj/PDh6OZr4EguC99TxlVNChIr7nPr3/OiAssbkvEnhlSLeABFO9+7KfutL2WhAjpFXTjtPVq6Qalc8UW0K0gxq//sVfhb1MzjenmdOf06uB2bilQ8kgwHo7dDdRZBqqAtxQ6Q0Ht3SFMj6v/1zVD3s+YX/kWCEbUTHm6r2G/eF794ozcJyU+6j1L8hm6mvf8Mr9XCqBfgpZy6FCLX+9OKdMvX2jY8reo3Xz1PA9R6yzhN08vjku+jW+fsoYrBLd0fY1UGK2uOuvBByCeJzXupd3YpBMjEyRupVxqEj7K0GWOJeml65mkqKSNsHdDSeSjMpb8mwneZyTbdjsxCFQRLcLgpAajFrkk4G2Yz3KfhXSo29XKEGX+EbY5NuP8KmDsBsguPI0zfwv/co0hAY8PIIcehxcdoR9Vb2c=
@@ -48,6 +50,7 @@ deploy:
go: 1.4.2
condition: $TRAVIS_DB = mysql
+# S3 deploy for master docker image. (latest compiled bits)
- provider: s3
access_key_id: AKIAJCO3KJYEGWJIKDIQ
secret_access_key:
@@ -63,3 +66,20 @@ deploy:
branch: master
go: 1.4.2
condition: $TRAVIS_DB = mysql
+
+# S3 deploy for documentation
+ - provider: s3
+ access_key_id: AKIAJCO3KJYEGWJIKDIQ
+ secret_access_key:
+ secure: p66X2tJBmKgtcVyPtGgkAwW29IiRojqGA39RjCJkIWNTJ0e/9JvBOiMS2c4a7I4aOads38rsthwdaigBWagDWNH7bGsEZN7B0TszZuFAuU+XGjU5A66MIOfFfzbUg8AnByysr+XG5/bknFIrP/XhM2fbRr6gbYrFUK7TNkpgjFs5u3BzUrz2iTAV8uOpSJqKSnaf0pTZk1EywOK/X8W8ViIjc7Di3FzQcqIW9K3D27N+3rVsv8SRT1hWASVlnG6aThqqebiM8FCGCzAYVgQb3h3Wu8JT5fIz7Qo7A6siVRwNBwWwzP8HkGoinEK32Wsj/fDXk27vjpFQO/+9sV0xfcTbIZA6MnuYWF4rHOT59KcshCWCD3V0FopX57p/dtOzM9+6lxIctAT++izxWoZit/5c5A4633iY1d+RMeTko1POix6MSlxPMRHZUFwSXROgFuWWRpyD6TlUTCST9/wTTd0WDPklAAiYcnuEPW3qCnw0r0xkrA4AwWUXqXdAIwDt5bA27KcjRyY4Fofv9NxH09BNuBTXNPrvnYPZMmaKrv+HOX3NFTreuV6+5LJdhYUxYSBvSWo1jeWIQ5Q9RUdTU0PqmKpMhJKbKey/S4gxCXHg2HR8DwLCcbIZcvneF9yPEAT71YA6zpLKoPVSwWwH97huKSzjpic/RUfFXQOcgCQ=
+ bucket: docs.mattermost.org
+ local_dir: documentation-html
+ acl: public_read
+ region: us-east-1
+ skip_cleanup: true
+ detect_encoding: true
+ on:
+ repo: mattermost/platform
+ branch: master
+ go: 1.4.2
+ condition: $TRAVIS_DB = mysql
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ee47ec35..7357796e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,28 +1,63 @@
# Mattermost Changelog
-## UNDER DEVELOPMENT - Release v1.0.0
+## Release v1.0.0
-The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the product's `master` branch to note key changes committed to master and are on their way to the next stable release. When a stable release is pushed the "UNDER DEVELOPMENT" heading is removed from the final changelog of the release.
-
-- **Release candidate anticipated:** September 28, 2015
-- **Final release anticipated:** October 2, 2015
+Released 2015-10-02
### Release Highlights
-- [See Product Roadmap for anticipated features](https://mattermost.atlassian.net/issues/?filter=10002)
+#### Markdown
+
+Markdown support is now available across messages, comments and channel descriptions for:
+
+- **Headings** - in five different sizes to help organize your thoughts
+- **Lists** - both numbered and bullets
+- **Font formatting** - including **bold**, _italics_, ~~strikethrough~~, `code`, links, and block quotes)
+- **In-line images** - useful for creating buttons and status messages
+- **Tables** - for keeping things organized
+- **Emoticons** - translation of emoji codes to images like :sheep: :boom: :rage1: :+1:
+
+See [documentation](doc/help/enduser/markdown.md) for full details.
+
+#### Themes
+
+Themes as been significantly upgraded in this release with:
+
+- 4 pre-set themes, two light and two dark, to customize your experience
+- 18 detailed color setting options to precisely match the colors of your other tools or preferences
+- Ability to import themes from Slack
+
+#### System console and command line tools
+
+Added new web-based System Console for managing instance level configuration. This lets IT admins conveniently:
+
+- _access core settings_, like server, database, email, rate limiting, file store, SSO, and log settings,
+- _monitor operations_, by quickly accessing log files and user roles, and
+- _manage teams_, with essential functions such as team role assignment and password reset
+
+In addition new command line tools are available for managing Mattermost system roles, creating users, resetting passwords, getting version info and other basic tasks.
+
+Run `./platform -h` for documentation using the new command line tool.
+
### New Features
Messaging, Comments and Notifications
-- Support for emoji codes rendering to image files
- Full markdown support in messages, comments, and channel description
+- Support for emoji codes rendering to image files
+
+
+Files and Images
+
- Added ability to play video and audio files
System Console
- UI to change config.json settings
- Ability to view log files from console
+- Ability to reset user passwords
+- Ability for IT admin to manage members across multiple teams from single interface
User Interface
@@ -31,12 +66,14 @@ User Interface
- Added ability to import themes from Slack
Integrations
+
- (Preview) Initial support for incoming webhooks
### Improvements
Documentation
+- Added production installation instructions
- Updated software and hardware requirements documentation
- Re-organized install instructions out of README and into separate files
- Added Code Contribution Guidelines
@@ -47,24 +84,29 @@ Documentation
Performance
- Enabled Javascript optimizations
+- Numerous improvements in center channel and mobile web
Code Quality
- Reformatted Javascript per Mattermost Style Guide
-
-UI
+
+User Interface
- Added version, build number, build date and build hash under Account Settings -> Security
+Licensing
+
+- Compiled version of Mattermost v1.0.0 now available under MIT license
+
### Bug Fixes
-- Numerous performance improvements
- Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously)
### Contributors
Many thanks to our external contributors. In no particular order:
+- [jdeng](https://github.com/jdeng)
- [Trozz](https://github.com/Trozz)
- [LAndres](https://github.com/LAndreas)
- [JessBot](https://github.com/JessBot)
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index 9bd02d455..7c80558c4 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -13,11 +13,6 @@
"Rev": "69e2a90ed92d03812364aeb947b7068dc42e561e"
},
{
- "ImportPath": "code.google.com/p/graphics-go/graphics",
- "Comment": "null-25",
- "Rev": "f843bfcd8ac420072c7f6804599995b0a229070b"
- },
- {
"ImportPath": "code.google.com/p/log4go",
"Comment": "go.weekly.2012-02-22-1",
"Rev": "c3294304d93f48a37d3bed1d382882a9c2989f99"
@@ -32,6 +27,10 @@
"Rev": "5280e250f2795914acbeb2bf3b55dd5a2d1fba52"
},
{
+ "ImportPath": "github.com/disintegration/imaging",
+ "Rev": "493653de80c32beeae336f3a3a3a125e7603459b"
+ },
+ {
"ImportPath": "github.com/garyburd/redigo/internal",
"Rev": "a47585eaae68b1d14b02940d2af1b9194f3caa9c"
},
@@ -89,10 +88,6 @@
"Rev": "a163d6a569f1cd264d2f8b2bf3c5d04ace5995eb"
},
{
- "ImportPath": "github.com/nfnt/resize",
- "Rev": "dc93e1b98c579d90ee2fa15c1fd6dac34f6e7899"
- },
- {
"ImportPath": "github.com/rwcarlsen/goexif/exif",
"Rev": "709fab3d192d7c62f86043caff1e7e3fb0f42bd8"
},
@@ -137,6 +132,10 @@
"Rev": "baddd3465a05d84a6d8d3507547a91cb188c81ea"
},
{
+ "ImportPath": "golang.org/x/image/tiff",
+ "Rev": "baddd3465a05d84a6d8d3507547a91cb188c81ea"
+ },
+ {
"ImportPath": "gopkg.in/fsnotify.v1",
"Comment": "v1.2.0",
"Rev": "96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0"
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/Makefile
deleted file mode 100644
index 28a06f0e8..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/Makefile
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright 2011 The Graphics-Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-include $(GOROOT)/src/Make.inc
-
-TARG=code.google.com/p/graphics-go/graphics
-GOFILES=\
- affine.go\
- blur.go\
- rotate.go\
- scale.go\
- thumbnail.go\
-
-include $(GOROOT)/src/Make.pkg
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/affine.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/affine.go
deleted file mode 100644
index 0ac2ec9da..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/affine.go
+++ /dev/null
@@ -1,174 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "code.google.com/p/graphics-go/graphics/interp"
- "errors"
- "image"
- "image/draw"
- "math"
-)
-
-// I is the identity Affine transform matrix.
-var I = Affine{
- 1, 0, 0,
- 0, 1, 0,
- 0, 0, 1,
-}
-
-// Affine is a 3x3 2D affine transform matrix.
-// M(i,j) is Affine[i*3+j].
-type Affine [9]float64
-
-// Mul returns the multiplication of two affine transform matrices.
-func (a Affine) Mul(b Affine) Affine {
- return Affine{
- a[0]*b[0] + a[1]*b[3] + a[2]*b[6],
- a[0]*b[1] + a[1]*b[4] + a[2]*b[7],
- a[0]*b[2] + a[1]*b[5] + a[2]*b[8],
- a[3]*b[0] + a[4]*b[3] + a[5]*b[6],
- a[3]*b[1] + a[4]*b[4] + a[5]*b[7],
- a[3]*b[2] + a[4]*b[5] + a[5]*b[8],
- a[6]*b[0] + a[7]*b[3] + a[8]*b[6],
- a[6]*b[1] + a[7]*b[4] + a[8]*b[7],
- a[6]*b[2] + a[7]*b[5] + a[8]*b[8],
- }
-}
-
-func (a Affine) transformRGBA(dst *image.RGBA, src *image.RGBA, i interp.RGBA) error {
- srcb := src.Bounds()
- b := dst.Bounds()
- for y := b.Min.Y; y < b.Max.Y; y++ {
- for x := b.Min.X; x < b.Max.X; x++ {
- sx, sy := a.pt(x, y)
- if inBounds(srcb, sx, sy) {
- c := i.RGBA(src, sx, sy)
- off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
- dst.Pix[off+0] = c.R
- dst.Pix[off+1] = c.G
- dst.Pix[off+2] = c.B
- dst.Pix[off+3] = c.A
- }
- }
- }
- return nil
-}
-
-// Transform applies the affine transform to src and produces dst.
-func (a Affine) Transform(dst draw.Image, src image.Image, i interp.Interp) error {
- if dst == nil {
- return errors.New("graphics: dst is nil")
- }
- if src == nil {
- return errors.New("graphics: src is nil")
- }
-
- // RGBA fast path.
- dstRGBA, dstOk := dst.(*image.RGBA)
- srcRGBA, srcOk := src.(*image.RGBA)
- interpRGBA, interpOk := i.(interp.RGBA)
- if dstOk && srcOk && interpOk {
- return a.transformRGBA(dstRGBA, srcRGBA, interpRGBA)
- }
-
- srcb := src.Bounds()
- b := dst.Bounds()
- for y := b.Min.Y; y < b.Max.Y; y++ {
- for x := b.Min.X; x < b.Max.X; x++ {
- sx, sy := a.pt(x, y)
- if inBounds(srcb, sx, sy) {
- dst.Set(x, y, i.Interp(src, sx, sy))
- }
- }
- }
- return nil
-}
-
-func inBounds(b image.Rectangle, x, y float64) bool {
- if x < float64(b.Min.X) || x >= float64(b.Max.X) {
- return false
- }
- if y < float64(b.Min.Y) || y >= float64(b.Max.Y) {
- return false
- }
- return true
-}
-
-func (a Affine) pt(x0, y0 int) (x1, y1 float64) {
- fx := float64(x0) + 0.5
- fy := float64(y0) + 0.5
- x1 = fx*a[0] + fy*a[1] + a[2]
- y1 = fx*a[3] + fy*a[4] + a[5]
- return x1, y1
-}
-
-// TransformCenter applies the affine transform to src and produces dst.
-// Equivalent to
-// a.CenterFit(dst, src).Transform(dst, src, i).
-func (a Affine) TransformCenter(dst draw.Image, src image.Image, i interp.Interp) error {
- if dst == nil {
- return errors.New("graphics: dst is nil")
- }
- if src == nil {
- return errors.New("graphics: src is nil")
- }
-
- return a.CenterFit(dst.Bounds(), src.Bounds()).Transform(dst, src, i)
-}
-
-// Scale produces a scaling transform of factors x and y.
-func (a Affine) Scale(x, y float64) Affine {
- return a.Mul(Affine{
- 1 / x, 0, 0,
- 0, 1 / y, 0,
- 0, 0, 1,
- })
-}
-
-// Rotate produces a clockwise rotation transform of angle, in radians.
-func (a Affine) Rotate(angle float64) Affine {
- s, c := math.Sincos(angle)
- return a.Mul(Affine{
- +c, +s, +0,
- -s, +c, +0,
- +0, +0, +1,
- })
-}
-
-// Shear produces a shear transform by the slopes x and y.
-func (a Affine) Shear(x, y float64) Affine {
- d := 1 - x*y
- return a.Mul(Affine{
- +1 / d, -x / d, 0,
- -y / d, +1 / d, 0,
- 0, 0, 1,
- })
-}
-
-// Translate produces a translation transform with pixel distances x and y.
-func (a Affine) Translate(x, y float64) Affine {
- return a.Mul(Affine{
- 1, 0, -x,
- 0, 1, -y,
- 0, 0, +1,
- })
-}
-
-// Center produces the affine transform, centered around the provided point.
-func (a Affine) Center(x, y float64) Affine {
- return I.Translate(-x, -y).Mul(a).Translate(x, y)
-}
-
-// CenterFit produces the affine transform, centered around the rectangles.
-// It is equivalent to
-// I.Translate(-<center of src>).Mul(a).Translate(<center of dst>)
-func (a Affine) CenterFit(dst, src image.Rectangle) Affine {
- dx := float64(dst.Min.X) + float64(dst.Dx())/2
- dy := float64(dst.Min.Y) + float64(dst.Dy())/2
- sx := float64(src.Min.X) + float64(src.Dx())/2
- sy := float64(src.Min.Y) + float64(src.Dy())/2
- return I.Translate(-sx, -sy).Mul(a).Translate(dx, dy)
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur.go
deleted file mode 100644
index 9a54d5ad5..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "code.google.com/p/graphics-go/graphics/convolve"
- "errors"
- "image"
- "image/draw"
- "math"
-)
-
-// DefaultStdDev is the default blurring parameter.
-var DefaultStdDev = 0.5
-
-// BlurOptions are the blurring parameters.
-// StdDev is the standard deviation of the normal, higher is blurrier.
-// Size is the size of the kernel. If zero, it is set to Ceil(6 * StdDev).
-type BlurOptions struct {
- StdDev float64
- Size int
-}
-
-// Blur produces a blurred version of the image, using a Gaussian blur.
-func Blur(dst draw.Image, src image.Image, opt *BlurOptions) error {
- if dst == nil {
- return errors.New("graphics: dst is nil")
- }
- if src == nil {
- return errors.New("graphics: src is nil")
- }
-
- sd := DefaultStdDev
- size := 0
-
- if opt != nil {
- sd = opt.StdDev
- size = opt.Size
- }
-
- if size < 1 {
- size = int(math.Ceil(sd * 6))
- }
-
- kernel := make([]float64, 2*size+1)
- for i := 0; i <= size; i++ {
- x := float64(i) / sd
- x = math.Pow(1/math.SqrtE, x*x)
- kernel[size-i] = x
- kernel[size+i] = x
- }
-
- // Normalize the weights to sum to 1.0.
- kSum := 0.0
- for _, k := range kernel {
- kSum += k
- }
- for i, k := range kernel {
- kernel[i] = k / kSum
- }
-
- return convolve.Convolve(dst, src, &convolve.SeparableKernel{
- X: kernel,
- Y: kernel,
- })
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur_test.go
deleted file mode 100644
index 1d84fa604..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur_test.go
+++ /dev/null
@@ -1,207 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "code.google.com/p/graphics-go/graphics/graphicstest"
- "image"
- "image/color"
- "testing"
-
- _ "image/png"
-)
-
-var blurOneColorTests = []transformOneColorTest{
- {
- "1x1-blank", 1, 1, 1, 1,
- &BlurOptions{0.83, 1},
- []uint8{0xff},
- []uint8{0xff},
- },
- {
- "1x1-spreadblank", 1, 1, 1, 1,
- &BlurOptions{0.83, 2},
- []uint8{0xff},
- []uint8{0xff},
- },
- {
- "3x3-blank", 3, 3, 3, 3,
- &BlurOptions{0.83, 2},
- []uint8{
- 0xff, 0xff, 0xff,
- 0xff, 0xff, 0xff,
- 0xff, 0xff, 0xff,
- },
- []uint8{
- 0xff, 0xff, 0xff,
- 0xff, 0xff, 0xff,
- 0xff, 0xff, 0xff,
- },
- },
- {
- "3x3-dot", 3, 3, 3, 3,
- &BlurOptions{0.34, 1},
- []uint8{
- 0x00, 0x00, 0x00,
- 0x00, 0xff, 0x00,
- 0x00, 0x00, 0x00,
- },
- []uint8{
- 0x00, 0x03, 0x00,
- 0x03, 0xf2, 0x03,
- 0x00, 0x03, 0x00,
- },
- },
- {
- "5x5-dot", 5, 5, 5, 5,
- &BlurOptions{0.34, 1},
- []uint8{
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0xff, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- },
- []uint8{
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x03, 0x00, 0x00,
- 0x00, 0x03, 0xf2, 0x03, 0x00,
- 0x00, 0x00, 0x03, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- },
- },
- {
- "5x5-dot-spread", 5, 5, 5, 5,
- &BlurOptions{0.85, 1},
- []uint8{
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0xff, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- },
- []uint8{
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x10, 0x20, 0x10, 0x00,
- 0x00, 0x20, 0x40, 0x20, 0x00,
- 0x00, 0x10, 0x20, 0x10, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- },
- },
- {
- "4x4-box", 4, 4, 4, 4,
- &BlurOptions{0.34, 1},
- []uint8{
- 0x00, 0x00, 0x00, 0x00,
- 0x00, 0xff, 0xff, 0x00,
- 0x00, 0xff, 0xff, 0x00,
- 0x00, 0x00, 0x00, 0x00,
- },
- []uint8{
- 0x00, 0x03, 0x03, 0x00,
- 0x03, 0xf8, 0xf8, 0x03,
- 0x03, 0xf8, 0xf8, 0x03,
- 0x00, 0x03, 0x03, 0x00,
- },
- },
- {
- "5x5-twodots", 5, 5, 5, 5,
- &BlurOptions{0.34, 1},
- []uint8{
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x96, 0x00, 0x96, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- },
- []uint8{
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x02, 0x00, 0x02, 0x00,
- 0x02, 0x8e, 0x04, 0x8e, 0x02,
- 0x00, 0x02, 0x00, 0x02, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- },
- },
-}
-
-func TestBlurOneColor(t *testing.T) {
- for _, oc := range blurOneColorTests {
- dst := oc.newDst()
- src := oc.newSrc()
- opt := oc.opt.(*BlurOptions)
- if err := Blur(dst, src, opt); err != nil {
- t.Fatal(err)
- }
-
- if !checkTransformTest(t, &oc, dst) {
- continue
- }
- }
-}
-
-func TestBlurEmpty(t *testing.T) {
- empty := image.NewRGBA(image.Rect(0, 0, 0, 0))
- if err := Blur(empty, empty, nil); err != nil {
- t.Fatal(err)
- }
-}
-
-func TestBlurGopher(t *testing.T) {
- src, err := graphicstest.LoadImage("../testdata/gopher.png")
- if err != nil {
- t.Fatal(err)
- }
-
- dst := image.NewRGBA(src.Bounds())
- if err = Blur(dst, src, &BlurOptions{StdDev: 1.1}); err != nil {
- t.Fatal(err)
- }
-
- cmp, err := graphicstest.LoadImage("../testdata/gopher-blur.png")
- if err != nil {
- t.Fatal(err)
- }
- err = graphicstest.ImageWithinTolerance(dst, cmp, 0x101)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func benchBlur(b *testing.B, bounds image.Rectangle) {
- b.StopTimer()
-
- // Construct a fuzzy image.
- src := image.NewRGBA(bounds)
- for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
- for x := bounds.Min.X; x < bounds.Max.X; x++ {
- src.SetRGBA(x, y, color.RGBA{
- uint8(5 * x % 0x100),
- uint8(7 * y % 0x100),
- uint8((7*x + 5*y) % 0x100),
- 0xff,
- })
- }
- }
- dst := image.NewRGBA(bounds)
-
- b.StartTimer()
- for i := 0; i < b.N; i++ {
- Blur(dst, src, &BlurOptions{0.84, 3})
- }
-}
-
-func BenchmarkBlur400x400x3(b *testing.B) {
- benchBlur(b, image.Rect(0, 0, 400, 400))
-}
-
-// Exactly twice the pixel count of 400x400.
-func BenchmarkBlur400x800x3(b *testing.B) {
- benchBlur(b, image.Rect(0, 0, 400, 800))
-}
-
-// Exactly twice the pixel count of 400x800
-func BenchmarkBlur400x1600x3(b *testing.B) {
- benchBlur(b, image.Rect(0, 0, 400, 1600))
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/Makefile
deleted file mode 100644
index a5691fa30..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/Makefile
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright 2011 The Graphics-Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-include $(GOROOT)/src/Make.inc
-
-TARG=code.google.com/p/graphics-go/graphics/convolve
-GOFILES=\
- convolve.go\
-
-include $(GOROOT)/src/Make.pkg
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve.go
deleted file mode 100644
index da69496d0..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve.go
+++ /dev/null
@@ -1,274 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package convolve
-
-import (
- "errors"
- "fmt"
- "image"
- "image/draw"
- "math"
-)
-
-// clamp clamps x to the range [x0, x1].
-func clamp(x, x0, x1 float64) float64 {
- if x < x0 {
- return x0
- }
- if x > x1 {
- return x1
- }
- return x
-}
-
-// Kernel is a square matrix that defines a convolution.
-type Kernel interface {
- // Weights returns the square matrix of weights in row major order.
- Weights() []float64
-}
-
-// SeparableKernel is a linearly separable, square convolution kernel.
-// X and Y are the per-axis weights. Each slice must be the same length, and
-// have an odd length. The middle element of each slice is the weight for the
-// central pixel. For example, the horizontal Sobel kernel is:
-// sobelX := &SeparableKernel{
-// X: []float64{-1, 0, +1},
-// Y: []float64{1, 2, 1},
-// }
-type SeparableKernel struct {
- X, Y []float64
-}
-
-func (k *SeparableKernel) Weights() []float64 {
- n := len(k.X)
- w := make([]float64, n*n)
- for y := 0; y < n; y++ {
- for x := 0; x < n; x++ {
- w[y*n+x] = k.X[x] * k.Y[y]
- }
- }
- return w
-}
-
-// fullKernel is a square convolution kernel.
-type fullKernel []float64
-
-func (k fullKernel) Weights() []float64 { return k }
-
-func kernelSize(w []float64) (size int, err error) {
- size = int(math.Sqrt(float64(len(w))))
- if size*size != len(w) {
- return 0, errors.New("graphics: kernel is not square")
- }
- if size%2 != 1 {
- return 0, errors.New("graphics: kernel size is not odd")
- }
- return size, nil
-}
-
-// NewKernel returns a square convolution kernel.
-func NewKernel(w []float64) (Kernel, error) {
- if _, err := kernelSize(w); err != nil {
- return nil, err
- }
- return fullKernel(w), nil
-}
-
-func convolveRGBASep(dst *image.RGBA, src image.Image, k *SeparableKernel) error {
- if len(k.X) != len(k.Y) {
- return fmt.Errorf("graphics: kernel not square (x %d, y %d)", len(k.X), len(k.Y))
- }
- if len(k.X)%2 != 1 {
- return fmt.Errorf("graphics: kernel length (%d) not odd", len(k.X))
- }
- radius := (len(k.X) - 1) / 2
-
- // buf holds the result of vertically blurring src.
- bounds := dst.Bounds()
- width, height := bounds.Dx(), bounds.Dy()
- buf := make([]float64, width*height*4)
- for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
- for x := bounds.Min.X; x < bounds.Max.X; x++ {
- var r, g, b, a float64
- // k0 is the kernel weight for the center pixel. This may be greater
- // than kernel[0], near the boundary of the source image, to avoid
- // vignetting.
- k0 := k.X[radius]
-
- // Add the pixels from above.
- for i := 1; i <= radius; i++ {
- f := k.Y[radius-i]
- if y-i < bounds.Min.Y {
- k0 += f
- } else {
- or, og, ob, oa := src.At(x, y-i).RGBA()
- r += float64(or>>8) * f
- g += float64(og>>8) * f
- b += float64(ob>>8) * f
- a += float64(oa>>8) * f
- }
- }
-
- // Add the pixels from below.
- for i := 1; i <= radius; i++ {
- f := k.Y[radius+i]
- if y+i >= bounds.Max.Y {
- k0 += f
- } else {
- or, og, ob, oa := src.At(x, y+i).RGBA()
- r += float64(or>>8) * f
- g += float64(og>>8) * f
- b += float64(ob>>8) * f
- a += float64(oa>>8) * f
- }
- }
-
- // Add the central pixel.
- or, og, ob, oa := src.At(x, y).RGBA()
- r += float64(or>>8) * k0
- g += float64(og>>8) * k0
- b += float64(ob>>8) * k0
- a += float64(oa>>8) * k0
-
- // Write to buf.
- o := (y-bounds.Min.Y)*width*4 + (x-bounds.Min.X)*4
- buf[o+0] = r
- buf[o+1] = g
- buf[o+2] = b
- buf[o+3] = a
- }
- }
-
- // dst holds the result of horizontally blurring buf.
- for y := 0; y < height; y++ {
- for x := 0; x < width; x++ {
- var r, g, b, a float64
- k0, off := k.X[radius], y*width*4+x*4
-
- // Add the pixels from the left.
- for i := 1; i <= radius; i++ {
- f := k.X[radius-i]
- if x-i < 0 {
- k0 += f
- } else {
- o := off - i*4
- r += buf[o+0] * f
- g += buf[o+1] * f
- b += buf[o+2] * f
- a += buf[o+3] * f
- }
- }
-
- // Add the pixels from the right.
- for i := 1; i <= radius; i++ {
- f := k.X[radius+i]
- if x+i >= width {
- k0 += f
- } else {
- o := off + i*4
- r += buf[o+0] * f
- g += buf[o+1] * f
- b += buf[o+2] * f
- a += buf[o+3] * f
- }
- }
-
- // Add the central pixel.
- r += buf[off+0] * k0
- g += buf[off+1] * k0
- b += buf[off+2] * k0
- a += buf[off+3] * k0
-
- // Write to dst, clamping to the range [0, 255].
- dstOff := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
- dst.Pix[dstOff+0] = uint8(clamp(r+0.5, 0, 255))
- dst.Pix[dstOff+1] = uint8(clamp(g+0.5, 0, 255))
- dst.Pix[dstOff+2] = uint8(clamp(b+0.5, 0, 255))
- dst.Pix[dstOff+3] = uint8(clamp(a+0.5, 0, 255))
- }
- }
-
- return nil
-}
-
-func convolveRGBA(dst *image.RGBA, src image.Image, k Kernel) error {
- b := dst.Bounds()
- bs := src.Bounds()
- w := k.Weights()
- size, err := kernelSize(w)
- if err != nil {
- return err
- }
- radius := (size - 1) / 2
-
- for y := b.Min.Y; y < b.Max.Y; y++ {
- for x := b.Min.X; x < b.Max.X; x++ {
- if !image.Pt(x, y).In(bs) {
- continue
- }
-
- var r, g, b, a, adj float64
- for cy := y - radius; cy <= y+radius; cy++ {
- for cx := x - radius; cx <= x+radius; cx++ {
- factor := w[(cy-y+radius)*size+cx-x+radius]
- if !image.Pt(cx, cy).In(bs) {
- adj += factor
- } else {
- sr, sg, sb, sa := src.At(cx, cy).RGBA()
- r += float64(sr>>8) * factor
- g += float64(sg>>8) * factor
- b += float64(sb>>8) * factor
- a += float64(sa>>8) * factor
- }
- }
- }
-
- if adj != 0 {
- sr, sg, sb, sa := src.At(x, y).RGBA()
- r += float64(sr>>8) * adj
- g += float64(sg>>8) * adj
- b += float64(sb>>8) * adj
- a += float64(sa>>8) * adj
- }
-
- off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
- dst.Pix[off+0] = uint8(clamp(r+0.5, 0, 0xff))
- dst.Pix[off+1] = uint8(clamp(g+0.5, 0, 0xff))
- dst.Pix[off+2] = uint8(clamp(b+0.5, 0, 0xff))
- dst.Pix[off+3] = uint8(clamp(a+0.5, 0, 0xff))
- }
- }
-
- return nil
-}
-
-// Convolve produces dst by applying the convolution kernel k to src.
-func Convolve(dst draw.Image, src image.Image, k Kernel) (err error) {
- if dst == nil || src == nil || k == nil {
- return nil
- }
-
- b := dst.Bounds()
- dstRgba, ok := dst.(*image.RGBA)
- if !ok {
- dstRgba = image.NewRGBA(b)
- }
-
- switch k := k.(type) {
- case *SeparableKernel:
- err = convolveRGBASep(dstRgba, src, k)
- default:
- err = convolveRGBA(dstRgba, src, k)
- }
-
- if err != nil {
- return err
- }
-
- if !ok {
- draw.Draw(dst, b, dstRgba, b.Min, draw.Src)
- }
- return nil
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve_test.go
deleted file mode 100644
index f34d7afc8..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package convolve
-
-import (
- "code.google.com/p/graphics-go/graphics/graphicstest"
- "image"
- "reflect"
- "testing"
-
- _ "image/png"
-)
-
-func TestSeparableWeights(t *testing.T) {
- sobelXFull := []float64{
- -1, 0, 1,
- -2, 0, 2,
- -1, 0, 1,
- }
- sobelXSep := &SeparableKernel{
- X: []float64{-1, 0, +1},
- Y: []float64{1, 2, 1},
- }
- w := sobelXSep.Weights()
- if !reflect.DeepEqual(w, sobelXFull) {
- t.Errorf("got %v want %v", w, sobelXFull)
- }
-}
-
-func TestConvolve(t *testing.T) {
- kernFull, err := NewKernel([]float64{
- 0, 0, 0,
- 1, 1, 1,
- 0, 0, 0,
- })
- if err != nil {
- t.Fatal(err)
- }
-
- kernSep := &SeparableKernel{
- X: []float64{1, 1, 1},
- Y: []float64{0, 1, 0},
- }
-
- src, err := graphicstest.LoadImage("../../testdata/gopher.png")
- if err != nil {
- t.Fatal(err)
- }
- b := src.Bounds()
-
- sep := image.NewRGBA(b)
- if err = Convolve(sep, src, kernSep); err != nil {
- t.Fatal(err)
- }
-
- full := image.NewRGBA(b)
- Convolve(full, src, kernFull)
-
- err = graphicstest.ImageWithinTolerance(sep, full, 0x101)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func TestConvolveNil(t *testing.T) {
- if err := Convolve(nil, nil, nil); err != nil {
- t.Fatal(err)
- }
-}
-
-func TestConvolveEmpty(t *testing.T) {
- empty := image.NewRGBA(image.Rect(0, 0, 0, 0))
- if err := Convolve(empty, empty, nil); err != nil {
- t.Fatal(err)
- }
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/Makefile
deleted file mode 100644
index 0b1c6cb3e..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/Makefile
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright 2011 The Graphics-Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-include $(GOROOT)/src/Make.inc
-
-TARG=code.google.com/p/graphics-go/graphics
-GOFILES=\
- detect.go\
- doc.go\
- integral.go\
- opencv_parser.go\
- projector.go\
-
-include $(GOROOT)/src/Make.pkg
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect.go
deleted file mode 100644
index dde941cbe..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect.go
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package detect
-
-import (
- "image"
- "math"
-)
-
-// Feature is a Haar-like feature.
-type Feature struct {
- Rect image.Rectangle
- Weight float64
-}
-
-// Classifier is a set of features with a threshold.
-type Classifier struct {
- Feature []Feature
- Threshold float64
- Left float64
- Right float64
-}
-
-// CascadeStage is a cascade of classifiers.
-type CascadeStage struct {
- Classifier []Classifier
- Threshold float64
-}
-
-// Cascade is a degenerate tree of Haar-like classifiers.
-type Cascade struct {
- Stage []CascadeStage
- Size image.Point
-}
-
-// Match returns true if the full image is classified as an object.
-func (c *Cascade) Match(m image.Image) bool {
- return c.classify(newWindow(m))
-}
-
-// Find returns a set of areas of m that match the feature cascade c.
-func (c *Cascade) Find(m image.Image) []image.Rectangle {
- // TODO(crawshaw): Consider de-duping strategies.
- matches := []image.Rectangle{}
- w := newWindow(m)
-
- b := m.Bounds()
- origScale := c.Size
- for s := origScale; s.X < b.Dx() && s.Y < b.Dy(); s = s.Add(s.Div(10)) {
- // translate region and classify
- tx := image.Pt(s.X/10, 0)
- ty := image.Pt(0, s.Y/10)
- for r := image.Rect(0, 0, s.X, s.Y).Add(b.Min); r.In(b); r = r.Add(ty) {
- for r1 := r; r1.In(b); r1 = r1.Add(tx) {
- if c.classify(w.subWindow(r1)) {
- matches = append(matches, r1)
- }
- }
- }
- }
- return matches
-}
-
-type window struct {
- mi *integral
- miSq *integral
- rect image.Rectangle
- invArea float64
- stdDev float64
-}
-
-func (w *window) init() {
- w.invArea = 1 / float64(w.rect.Dx()*w.rect.Dy())
- mean := float64(w.mi.sum(w.rect)) * w.invArea
- vr := float64(w.miSq.sum(w.rect))*w.invArea - mean*mean
- if vr < 0 {
- vr = 1
- }
- w.stdDev = math.Sqrt(vr)
-}
-
-func newWindow(m image.Image) *window {
- mi, miSq := newIntegrals(m)
- res := &window{
- mi: mi,
- miSq: miSq,
- rect: m.Bounds(),
- }
- res.init()
- return res
-}
-
-func (w *window) subWindow(r image.Rectangle) *window {
- res := &window{
- mi: w.mi,
- miSq: w.miSq,
- rect: r,
- }
- res.init()
- return res
-}
-
-func (c *Classifier) classify(w *window, pr *projector) float64 {
- s := 0.0
- for _, f := range c.Feature {
- s += float64(w.mi.sum(pr.rect(f.Rect))) * f.Weight
- }
- s *= w.invArea // normalize to maintain scale invariance
- if s < c.Threshold*w.stdDev {
- return c.Left
- }
- return c.Right
-}
-
-func (s *CascadeStage) classify(w *window, pr *projector) bool {
- sum := 0.0
- for _, c := range s.Classifier {
- sum += c.classify(w, pr)
- }
- return sum >= s.Threshold
-}
-
-func (c *Cascade) classify(w *window) bool {
- pr := newProjector(w.rect, image.Rectangle{image.Pt(0, 0), c.Size})
- for _, s := range c.Stage {
- if !s.classify(w, pr) {
- return false
- }
- }
- return true
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect_test.go
deleted file mode 100644
index 8a2df113d..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect_test.go
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package detect
-
-import (
- "image"
- "image/draw"
- "testing"
-)
-
-var (
- c0 = Classifier{
- Feature: []Feature{
- Feature{Rect: image.Rect(0, 0, 3, 4), Weight: 4.0},
- },
- Threshold: 0.2,
- Left: 0.8,
- Right: 0.2,
- }
- c1 = Classifier{
- Feature: []Feature{
- Feature{Rect: image.Rect(3, 4, 4, 5), Weight: 4.0},
- },
- Threshold: 0.2,
- Left: 0.8,
- Right: 0.2,
- }
- c2 = Classifier{
- Feature: []Feature{
- Feature{Rect: image.Rect(0, 0, 1, 1), Weight: +4.0},
- Feature{Rect: image.Rect(0, 0, 2, 2), Weight: -1.0},
- },
- Threshold: 0.2,
- Left: 0.8,
- Right: 0.2,
- }
-)
-
-func TestClassifier(t *testing.T) {
- m := image.NewGray(image.Rect(0, 0, 20, 20))
- b := m.Bounds()
- draw.Draw(m, image.Rect(0, 0, 20, 20), image.White, image.ZP, draw.Src)
- draw.Draw(m, image.Rect(3, 4, 4, 5), image.Black, image.ZP, draw.Src)
- w := newWindow(m)
- pr := newProjector(b, b)
-
- if res := c0.classify(w, pr); res != c0.Right {
- t.Errorf("c0 got %f want %f", res, c0.Right)
- }
- if res := c1.classify(w, pr); res != c1.Left {
- t.Errorf("c1 got %f want %f", res, c1.Left)
- }
- if res := c2.classify(w, pr); res != c1.Left {
- t.Errorf("c2 got %f want %f", res, c1.Left)
- }
-}
-
-func TestClassifierScale(t *testing.T) {
- m := image.NewGray(image.Rect(0, 0, 50, 50))
- b := m.Bounds()
- draw.Draw(m, image.Rect(0, 0, 8, 10), image.White, b.Min, draw.Src)
- draw.Draw(m, image.Rect(8, 10, 10, 13), image.Black, b.Min, draw.Src)
- w := newWindow(m)
- pr := newProjector(b, image.Rect(0, 0, 20, 20))
-
- if res := c0.classify(w, pr); res != c0.Right {
- t.Errorf("scaled c0 got %f want %f", res, c0.Right)
- }
- if res := c1.classify(w, pr); res != c1.Left {
- t.Errorf("scaled c1 got %f want %f", res, c1.Left)
- }
- if res := c2.classify(w, pr); res != c1.Left {
- t.Errorf("scaled c2 got %f want %f", res, c1.Left)
- }
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/doc.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/doc.go
deleted file mode 100644
index a0f4e94cd..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/doc.go
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-/*
-Package detect implements an object detector cascade.
-
-The technique used is a degenerate tree of Haar-like classifiers, commonly
-used for face detection. It is described in
-
- P. Viola, M. Jones.
- Rapid Object Detection using a Boosted Cascade of Simple Features, 2001
- IEEE Conference on Computer Vision and Pattern Recognition
-
-A Cascade can be constructed manually from a set of Classifiers in stages,
-or can be loaded from an XML file in the OpenCV format with
-
- classifier, _, err := detect.ParseOpenCV(r)
-
-The classifier can be used to determine if a full image is detected as an
-object using Detect
-
- if classifier.Match(m) {
- // m is an image of a face.
- }
-
-It is also possible to search an image for occurrences of an object
-
- objs := classifier.Find(m)
-*/
-package detect
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral.go
deleted file mode 100644
index 814ced590..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral.go
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package detect
-
-import (
- "image"
- "image/draw"
-)
-
-// integral is an image.Image-like structure that stores the cumulative
-// sum of the preceding pixels. This allows for O(1) summation of any
-// rectangular region within the image.
-type integral struct {
- // pix holds the cumulative sum of the image's pixels. The pixel at
- // (x, y) starts at pix[(y-rect.Min.Y)*stride + (x-rect.Min.X)*1].
- pix []uint64
- stride int
- rect image.Rectangle
-}
-
-func (p *integral) at(x, y int) uint64 {
- return p.pix[(y-p.rect.Min.Y)*p.stride+(x-p.rect.Min.X)]
-}
-
-func (p *integral) sum(b image.Rectangle) uint64 {
- c := p.at(b.Max.X-1, b.Max.Y-1)
- inY := b.Min.Y > p.rect.Min.Y
- inX := b.Min.X > p.rect.Min.X
- if inY && inX {
- c += p.at(b.Min.X-1, b.Min.Y-1)
- }
- if inY {
- c -= p.at(b.Max.X-1, b.Min.Y-1)
- }
- if inX {
- c -= p.at(b.Min.X-1, b.Max.Y-1)
- }
- return c
-}
-
-func (m *integral) integrate() {
- b := m.rect
- for y := b.Min.Y; y < b.Max.Y; y++ {
- for x := b.Min.X; x < b.Max.X; x++ {
- c := uint64(0)
- if y > b.Min.Y && x > b.Min.X {
- c += m.at(x-1, y)
- c += m.at(x, y-1)
- c -= m.at(x-1, y-1)
- } else if y > b.Min.Y {
- c += m.at(b.Min.X, y-1)
- } else if x > b.Min.X {
- c += m.at(x-1, b.Min.Y)
- }
- m.pix[(y-m.rect.Min.Y)*m.stride+(x-m.rect.Min.X)] += c
- }
- }
-}
-
-// newIntegrals returns the integral and the squared integral.
-func newIntegrals(src image.Image) (*integral, *integral) {
- b := src.Bounds()
- srcg, ok := src.(*image.Gray)
- if !ok {
- srcg = image.NewGray(b)
- draw.Draw(srcg, b, src, b.Min, draw.Src)
- }
-
- m := integral{
- pix: make([]uint64, b.Max.Y*b.Max.X),
- stride: b.Max.X,
- rect: b,
- }
- mSq := integral{
- pix: make([]uint64, b.Max.Y*b.Max.X),
- stride: b.Max.X,
- rect: b,
- }
- for y := b.Min.Y; y < b.Max.Y; y++ {
- for x := b.Min.X; x < b.Max.X; x++ {
- os := (y-b.Min.Y)*srcg.Stride + x - b.Min.X
- om := (y-b.Min.Y)*m.stride + x - b.Min.X
- c := uint64(srcg.Pix[os])
- m.pix[om] = c
- mSq.pix[om] = c * c
- }
- }
- m.integrate()
- mSq.integrate()
- return &m, &mSq
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral_test.go
deleted file mode 100644
index 0bc321a4d..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral_test.go
+++ /dev/null
@@ -1,156 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package detect
-
-import (
- "bytes"
- "fmt"
- "image"
- "testing"
-)
-
-type integralTest struct {
- x int
- y int
- src []uint8
- res []uint8
-}
-
-var integralTests = []integralTest{
- {
- 1, 1,
- []uint8{0x01},
- []uint8{0x01},
- },
- {
- 2, 2,
- []uint8{
- 0x01, 0x02,
- 0x03, 0x04,
- },
- []uint8{
- 0x01, 0x03,
- 0x04, 0x0a,
- },
- },
- {
- 4, 4,
- []uint8{
- 0x02, 0x03, 0x00, 0x01,
- 0x01, 0x02, 0x01, 0x05,
- 0x01, 0x01, 0x01, 0x01,
- 0x01, 0x01, 0x01, 0x01,
- },
- []uint8{
- 0x02, 0x05, 0x05, 0x06,
- 0x03, 0x08, 0x09, 0x0f,
- 0x04, 0x0a, 0x0c, 0x13,
- 0x05, 0x0c, 0x0f, 0x17,
- },
- },
-}
-
-func sprintBox(box []byte, width, height int) string {
- buf := bytes.NewBuffer(nil)
- i := 0
- for y := 0; y < height; y++ {
- for x := 0; x < width; x++ {
- fmt.Fprintf(buf, " 0x%02x,", box[i])
- i++
- }
- buf.WriteByte('\n')
- }
- return buf.String()
-}
-
-func TestIntegral(t *testing.T) {
- for i, oc := range integralTests {
- src := &image.Gray{
- Pix: oc.src,
- Stride: oc.x,
- Rect: image.Rect(0, 0, oc.x, oc.y),
- }
- dst, _ := newIntegrals(src)
- res := make([]byte, len(dst.pix))
- for i, p := range dst.pix {
- res[i] = byte(p)
- }
-
- if !bytes.Equal(res, oc.res) {
- got := sprintBox(res, oc.x, oc.y)
- want := sprintBox(oc.res, oc.x, oc.y)
- t.Errorf("%d: got\n%s\n want\n%s", i, got, want)
- }
- }
-}
-
-func TestIntegralSum(t *testing.T) {
- src := &image.Gray{
- Pix: []uint8{
- 0x02, 0x03, 0x00, 0x01, 0x03,
- 0x01, 0x02, 0x01, 0x05, 0x05,
- 0x01, 0x01, 0x01, 0x01, 0x02,
- 0x01, 0x01, 0x01, 0x01, 0x07,
- 0x02, 0x01, 0x00, 0x03, 0x01,
- },
- Stride: 5,
- Rect: image.Rect(0, 0, 5, 5),
- }
- img, _ := newIntegrals(src)
-
- type sumTest struct {
- rect image.Rectangle
- sum uint64
- }
-
- var sumTests = []sumTest{
- {image.Rect(0, 0, 1, 1), 2},
- {image.Rect(0, 0, 2, 1), 5},
- {image.Rect(0, 0, 1, 3), 4},
- {image.Rect(1, 1, 3, 3), 5},
- {image.Rect(2, 2, 4, 4), 4},
- {image.Rect(4, 3, 5, 5), 8},
- {image.Rect(2, 4, 3, 5), 0},
- }
-
- for _, st := range sumTests {
- s := img.sum(st.rect)
- if s != st.sum {
- t.Errorf("%v: got %d want %d", st.rect, s, st.sum)
- return
- }
- }
-}
-
-func TestIntegralSubImage(t *testing.T) {
- m0 := &image.Gray{
- Pix: []uint8{
- 0x02, 0x03, 0x00, 0x01, 0x03,
- 0x01, 0x02, 0x01, 0x05, 0x05,
- 0x01, 0x04, 0x01, 0x01, 0x02,
- 0x01, 0x02, 0x01, 0x01, 0x07,
- 0x02, 0x01, 0x09, 0x03, 0x01,
- },
- Stride: 5,
- Rect: image.Rect(0, 0, 5, 5),
- }
- b := image.Rect(1, 1, 4, 4)
- m1 := m0.SubImage(b)
- mi0, _ := newIntegrals(m0)
- mi1, _ := newIntegrals(m1)
-
- sum0 := mi0.sum(b)
- sum1 := mi1.sum(b)
- if sum0 != sum1 {
- t.Errorf("b got %d want %d", sum0, sum1)
- }
-
- r0 := image.Rect(2, 2, 4, 4)
- sum0 = mi0.sum(r0)
- sum1 = mi1.sum(r0)
- if sum0 != sum1 {
- t.Errorf("r0 got %d want %d", sum1, sum0)
- }
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser.go
deleted file mode 100644
index 51ded1a1c..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser.go
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package detect
-
-import (
- "bytes"
- "encoding/xml"
- "errors"
- "fmt"
- "image"
- "io"
- "io/ioutil"
- "strconv"
- "strings"
-)
-
-type xmlFeature struct {
- Rects []string `xml:"grp>feature>rects>grp"`
- Tilted int `xml:"grp>feature>tilted"`
- Threshold float64 `xml:"grp>threshold"`
- Left float64 `xml:"grp>left_val"`
- Right float64 `xml:"grp>right_val"`
-}
-
-type xmlStages struct {
- Trees []xmlFeature `xml:"trees>grp"`
- Stage_threshold float64 `xml:"stage_threshold"`
- Parent int `xml:"parent"`
- Next int `xml:"next"`
-}
-
-type opencv_storage struct {
- Any struct {
- XMLName xml.Name
- Type string `xml:"type_id,attr"`
- Size string `xml:"size"`
- Stages []xmlStages `xml:"stages>grp"`
- } `xml:",any"`
-}
-
-func buildFeature(r string) (f Feature, err error) {
- var x, y, w, h int
- var weight float64
- _, err = fmt.Sscanf(r, "%d %d %d %d %f", &x, &y, &w, &h, &weight)
- if err != nil {
- return
- }
- f.Rect = image.Rect(x, y, x+w, y+h)
- f.Weight = weight
- return
-}
-
-func buildCascade(s *opencv_storage) (c *Cascade, name string, err error) {
- if s.Any.Type != "opencv-haar-classifier" {
- err = fmt.Errorf("got %s want opencv-haar-classifier", s.Any.Type)
- return
- }
- name = s.Any.XMLName.Local
-
- c = &Cascade{}
- sizes := strings.Split(s.Any.Size, " ")
- w, err := strconv.Atoi(sizes[0])
- if err != nil {
- return nil, "", err
- }
- h, err := strconv.Atoi(sizes[1])
- if err != nil {
- return nil, "", err
- }
- c.Size = image.Pt(w, h)
- c.Stage = []CascadeStage{}
-
- for _, stage := range s.Any.Stages {
- cs := CascadeStage{
- Classifier: []Classifier{},
- Threshold: stage.Stage_threshold,
- }
- for _, tree := range stage.Trees {
- if tree.Tilted != 0 {
- err = errors.New("Cascade does not support tilted features")
- return
- }
-
- cls := Classifier{
- Feature: []Feature{},
- Threshold: tree.Threshold,
- Left: tree.Left,
- Right: tree.Right,
- }
-
- for _, rect := range tree.Rects {
- f, err := buildFeature(rect)
- if err != nil {
- return nil, "", err
- }
- cls.Feature = append(cls.Feature, f)
- }
-
- cs.Classifier = append(cs.Classifier, cls)
- }
- c.Stage = append(c.Stage, cs)
- }
-
- return
-}
-
-// ParseOpenCV produces a detection Cascade from an OpenCV XML file.
-func ParseOpenCV(r io.Reader) (cascade *Cascade, name string, err error) {
- // BUG(crawshaw): tag-based parsing doesn't seem to work with <_>
- buf, err := ioutil.ReadAll(r)
- if err != nil {
- return
- }
- buf = bytes.Replace(buf, []byte("<_>"), []byte("<grp>"), -1)
- buf = bytes.Replace(buf, []byte("</_>"), []byte("</grp>"), -1)
-
- s := &opencv_storage{}
- err = xml.Unmarshal(buf, s)
- if err != nil {
- return
- }
- return buildCascade(s)
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser_test.go
deleted file mode 100644
index 343390499..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package detect
-
-import (
- "image"
- "os"
- "reflect"
- "testing"
-)
-
-var (
- classifier0 = Classifier{
- Feature: []Feature{
- Feature{Rect: image.Rect(0, 0, 3, 4), Weight: -1},
- Feature{Rect: image.Rect(3, 4, 5, 6), Weight: 3.1},
- },
- Threshold: 0.03,
- Left: 0.01,
- Right: 0.8,
- }
- classifier1 = Classifier{
- Feature: []Feature{
- Feature{Rect: image.Rect(3, 7, 17, 11), Weight: -3.2},
- Feature{Rect: image.Rect(3, 9, 17, 11), Weight: 2.},
- },
- Threshold: 0.11,
- Left: 0.03,
- Right: 0.83,
- }
- classifier2 = Classifier{
- Feature: []Feature{
- Feature{Rect: image.Rect(1, 1, 3, 3), Weight: -1.},
- Feature{Rect: image.Rect(3, 3, 5, 5), Weight: 2.5},
- },
- Threshold: 0.07,
- Left: 0.2,
- Right: 0.4,
- }
- cascade = Cascade{
- Stage: []CascadeStage{
- CascadeStage{
- Classifier: []Classifier{classifier0, classifier1},
- Threshold: 0.82,
- },
- CascadeStage{
- Classifier: []Classifier{classifier2},
- Threshold: 0.22,
- },
- },
- Size: image.Pt(20, 20),
- }
-)
-
-func TestParseOpenCV(t *testing.T) {
- file, err := os.Open("../../testdata/opencv.xml")
- if err != nil {
- t.Fatal(err)
- }
- defer file.Close()
-
- cascadeFile, name, err := ParseOpenCV(file)
- if err != nil {
- t.Fatal(err)
- }
- if name != "name_of_cascade" {
- t.Fatalf("name: got %s want name_of_cascade", name)
- }
-
- if !reflect.DeepEqual(cascade, *cascadeFile) {
- t.Errorf("got\n %v want\n %v", *cascadeFile, cascade)
- }
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector.go
deleted file mode 100644
index 1ebd6db59..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector.go
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package detect
-
-import (
- "image"
-)
-
-// projector allows projecting from a source Rectangle onto a target Rectangle.
-type projector struct {
- // rx, ry is the scaling factor.
- rx, ry float64
- // dx, dy is the translation factor.
- dx, dy float64
- // r is the clipping region of the target.
- r image.Rectangle
-}
-
-// newProjector creates a Projector with source src and target dst.
-func newProjector(dst image.Rectangle, src image.Rectangle) *projector {
- return &projector{
- rx: float64(dst.Dx()) / float64(src.Dx()),
- ry: float64(dst.Dy()) / float64(src.Dy()),
- dx: float64(dst.Min.X - src.Min.X),
- dy: float64(dst.Min.Y - src.Min.Y),
- r: dst,
- }
-}
-
-// pt projects p from the source rectangle onto the target rectangle.
-func (s *projector) pt(p image.Point) image.Point {
- return image.Point{
- clamp(s.rx*float64(p.X)+s.dx, s.r.Min.X, s.r.Max.X),
- clamp(s.ry*float64(p.Y)+s.dy, s.r.Min.Y, s.r.Max.Y),
- }
-}
-
-// rect projects r from the source rectangle onto the target rectangle.
-func (s *projector) rect(r image.Rectangle) image.Rectangle {
- return image.Rectangle{s.pt(r.Min), s.pt(r.Max)}
-}
-
-// clamp rounds and clamps o to the integer range [x0, x1].
-func clamp(o float64, x0, x1 int) int {
- x := int(o + 0.5)
- if x < x0 {
- return x0
- }
- if x > x1 {
- return x1
- }
- return x
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector_test.go
deleted file mode 100644
index c6d0b0cd5..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package detect
-
-import (
- "image"
- "reflect"
- "testing"
-)
-
-type projectorTest struct {
- dst image.Rectangle
- src image.Rectangle
- pdst image.Rectangle
- psrc image.Rectangle
-}
-
-var projectorTests = []projectorTest{
- {
- image.Rect(0, 0, 6, 6),
- image.Rect(0, 0, 2, 2),
- image.Rect(0, 0, 6, 6),
- image.Rect(0, 0, 2, 2),
- },
- {
- image.Rect(0, 0, 6, 6),
- image.Rect(0, 0, 2, 2),
- image.Rect(3, 3, 6, 6),
- image.Rect(1, 1, 2, 2),
- },
- {
- image.Rect(30, 30, 40, 40),
- image.Rect(10, 10, 20, 20),
- image.Rect(32, 33, 34, 37),
- image.Rect(12, 13, 14, 17),
- },
-}
-
-func TestProjector(t *testing.T) {
- for i, tt := range projectorTests {
- pr := newProjector(tt.dst, tt.src)
- res := pr.rect(tt.psrc)
- if !reflect.DeepEqual(res, tt.pdst) {
- t.Errorf("%d: got %v want %v", i, res, tt.pdst)
- }
- }
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/Makefile
deleted file mode 100644
index 7bfdf22d8..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/Makefile
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright 2011 The Graphics-Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-include $(GOROOT)/src/Make.inc
-
-TARG=code.google.com/p/graphics-go/graphics/graphicstest
-GOFILES=\
- graphicstest.go\
-
-include $(GOROOT)/src/Make.pkg
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/graphicstest.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/graphicstest.go
deleted file mode 100644
index ceb3a974d..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/graphicstest.go
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphicstest
-
-import (
- "bytes"
- "errors"
- "fmt"
- "image"
- "image/color"
- "os"
-)
-
-// LoadImage decodes an image from a file.
-func LoadImage(path string) (img image.Image, err error) {
- file, err := os.Open(path)
- if err != nil {
- return
- }
- defer file.Close()
- img, _, err = image.Decode(file)
- return
-}
-
-func delta(u0, u1 uint32) int {
- d := int(u0) - int(u1)
- if d < 0 {
- return -d
- }
- return d
-}
-
-func withinTolerance(c0, c1 color.Color, tol int) bool {
- r0, g0, b0, a0 := c0.RGBA()
- r1, g1, b1, a1 := c1.RGBA()
- r := delta(r0, r1)
- g := delta(g0, g1)
- b := delta(b0, b1)
- a := delta(a0, a1)
- return r <= tol && g <= tol && b <= tol && a <= tol
-}
-
-// ImageWithinTolerance checks that each pixel varies by no more than tol.
-func ImageWithinTolerance(m0, m1 image.Image, tol int) error {
- b0 := m0.Bounds()
- b1 := m1.Bounds()
- if !b0.Eq(b1) {
- return errors.New(fmt.Sprintf("got bounds %v want %v", b0, b1))
- }
-
- for y := b0.Min.Y; y < b0.Max.Y; y++ {
- for x := b0.Min.X; x < b0.Max.X; x++ {
- c0 := m0.At(x, y)
- c1 := m1.At(x, y)
- if !withinTolerance(c0, c1, tol) {
- e := fmt.Sprintf("got %v want %v at (%d, %d)", c0, c1, x, y)
- return errors.New(e)
- }
- }
- }
- return nil
-}
-
-// SprintBox pretty prints the array as a hexidecimal matrix.
-func SprintBox(box []byte, width, height int) string {
- buf := bytes.NewBuffer(nil)
- i := 0
- for y := 0; y < height; y++ {
- for x := 0; x < width; x++ {
- fmt.Fprintf(buf, " 0x%02x,", box[i])
- i++
- }
- buf.WriteByte('\n')
- }
- return buf.String()
-}
-
-// SprintImageR pretty prints the red channel of src. It looks like SprintBox.
-func SprintImageR(src *image.RGBA) string {
- w, h := src.Rect.Dx(), src.Rect.Dy()
- i := 0
- box := make([]byte, w*h)
- for y := src.Rect.Min.Y; y < src.Rect.Max.Y; y++ {
- for x := src.Rect.Min.X; x < src.Rect.Max.X; x++ {
- off := (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4
- box[i] = src.Pix[off]
- i++
- }
- }
- return SprintBox(box, w, h)
-}
-
-// MakeRGBA returns an image with R, G, B taken from src.
-func MakeRGBA(src []uint8, width int) *image.RGBA {
- b := image.Rect(0, 0, width, len(src)/width)
- ret := image.NewRGBA(b)
- i := 0
- for y := b.Min.Y; y < b.Max.Y; y++ {
- for x := b.Min.X; x < b.Max.X; x++ {
- ret.SetRGBA(x, y, color.RGBA{
- R: src[i],
- G: src[i],
- B: src[i],
- A: 0xff,
- })
- i++
- }
- }
- return ret
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/Makefile
deleted file mode 100644
index 4d8f524fb..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/Makefile
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2012 The Graphics-Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-include $(GOROOT)/src/Make.inc
-
-TARG=code.google.com/p/graphics-go/graphics/interp
-GOFILES=\
- bilinear.go\
- doc.go\
- interp.go\
-
-include $(GOROOT)/src/Make.pkg
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear.go
deleted file mode 100644
index e18321a15..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear.go
+++ /dev/null
@@ -1,206 +0,0 @@
-// Copyright 2012 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package interp
-
-import (
- "image"
- "image/color"
- "math"
-)
-
-// Bilinear implements bilinear interpolation.
-var Bilinear Interp = bilinear{}
-
-type bilinear struct{}
-
-func (i bilinear) Interp(src image.Image, x, y float64) color.Color {
- if src, ok := src.(*image.RGBA); ok {
- return i.RGBA(src, x, y)
- }
- return bilinearGeneral(src, x, y)
-}
-
-func bilinearGeneral(src image.Image, x, y float64) color.Color {
- p := findLinearSrc(src.Bounds(), x, y)
- var fr, fg, fb, fa float64
- var r, g, b, a uint32
-
- r, g, b, a = src.At(p.low.X, p.low.Y).RGBA()
- fr += float64(r) * p.frac00
- fg += float64(g) * p.frac00
- fb += float64(b) * p.frac00
- fa += float64(a) * p.frac00
-
- r, g, b, a = src.At(p.high.X, p.low.Y).RGBA()
- fr += float64(r) * p.frac01
- fg += float64(g) * p.frac01
- fb += float64(b) * p.frac01
- fa += float64(a) * p.frac01
-
- r, g, b, a = src.At(p.low.X, p.high.Y).RGBA()
- fr += float64(r) * p.frac10
- fg += float64(g) * p.frac10
- fb += float64(b) * p.frac10
- fa += float64(a) * p.frac10
-
- r, g, b, a = src.At(p.high.X, p.high.Y).RGBA()
- fr += float64(r) * p.frac11
- fg += float64(g) * p.frac11
- fb += float64(b) * p.frac11
- fa += float64(a) * p.frac11
-
- var c color.RGBA64
- c.R = uint16(fr + 0.5)
- c.G = uint16(fg + 0.5)
- c.B = uint16(fb + 0.5)
- c.A = uint16(fa + 0.5)
- return c
-}
-
-func (bilinear) RGBA(src *image.RGBA, x, y float64) color.RGBA {
- p := findLinearSrc(src.Bounds(), x, y)
-
- // Array offsets for the surrounding pixels.
- off00 := offRGBA(src, p.low.X, p.low.Y)
- off01 := offRGBA(src, p.high.X, p.low.Y)
- off10 := offRGBA(src, p.low.X, p.high.Y)
- off11 := offRGBA(src, p.high.X, p.high.Y)
-
- var fr, fg, fb, fa float64
-
- fr += float64(src.Pix[off00+0]) * p.frac00
- fg += float64(src.Pix[off00+1]) * p.frac00
- fb += float64(src.Pix[off00+2]) * p.frac00
- fa += float64(src.Pix[off00+3]) * p.frac00
-
- fr += float64(src.Pix[off01+0]) * p.frac01
- fg += float64(src.Pix[off01+1]) * p.frac01
- fb += float64(src.Pix[off01+2]) * p.frac01
- fa += float64(src.Pix[off01+3]) * p.frac01
-
- fr += float64(src.Pix[off10+0]) * p.frac10
- fg += float64(src.Pix[off10+1]) * p.frac10
- fb += float64(src.Pix[off10+2]) * p.frac10
- fa += float64(src.Pix[off10+3]) * p.frac10
-
- fr += float64(src.Pix[off11+0]) * p.frac11
- fg += float64(src.Pix[off11+1]) * p.frac11
- fb += float64(src.Pix[off11+2]) * p.frac11
- fa += float64(src.Pix[off11+3]) * p.frac11
-
- var c color.RGBA
- c.R = uint8(fr + 0.5)
- c.G = uint8(fg + 0.5)
- c.B = uint8(fb + 0.5)
- c.A = uint8(fa + 0.5)
- return c
-}
-
-func (bilinear) Gray(src *image.Gray, x, y float64) color.Gray {
- p := findLinearSrc(src.Bounds(), x, y)
-
- // Array offsets for the surrounding pixels.
- off00 := offGray(src, p.low.X, p.low.Y)
- off01 := offGray(src, p.high.X, p.low.Y)
- off10 := offGray(src, p.low.X, p.high.Y)
- off11 := offGray(src, p.high.X, p.high.Y)
-
- var fc float64
- fc += float64(src.Pix[off00]) * p.frac00
- fc += float64(src.Pix[off01]) * p.frac01
- fc += float64(src.Pix[off10]) * p.frac10
- fc += float64(src.Pix[off11]) * p.frac11
-
- var c color.Gray
- c.Y = uint8(fc + 0.5)
- return c
-}
-
-type bilinearSrc struct {
- // Top-left and bottom-right interpolation sources
- low, high image.Point
- // Fraction of each pixel to take. The 0 suffix indicates
- // top/left, and the 1 suffix indicates bottom/right.
- frac00, frac01, frac10, frac11 float64
-}
-
-func findLinearSrc(b image.Rectangle, sx, sy float64) bilinearSrc {
- maxX := float64(b.Max.X)
- maxY := float64(b.Max.Y)
- minX := float64(b.Min.X)
- minY := float64(b.Min.Y)
- lowX := math.Floor(sx - 0.5)
- lowY := math.Floor(sy - 0.5)
- if lowX < minX {
- lowX = minX
- }
- if lowY < minY {
- lowY = minY
- }
-
- highX := math.Ceil(sx - 0.5)
- highY := math.Ceil(sy - 0.5)
- if highX >= maxX {
- highX = maxX - 1
- }
- if highY >= maxY {
- highY = maxY - 1
- }
-
- // In the variables below, the 0 suffix indicates top/left, and the
- // 1 suffix indicates bottom/right.
-
- // Center of each surrounding pixel.
- x00 := lowX + 0.5
- y00 := lowY + 0.5
- x01 := highX + 0.5
- y01 := lowY + 0.5
- x10 := lowX + 0.5
- y10 := highY + 0.5
- x11 := highX + 0.5
- y11 := highY + 0.5
-
- p := bilinearSrc{
- low: image.Pt(int(lowX), int(lowY)),
- high: image.Pt(int(highX), int(highY)),
- }
-
- // Literally, edge cases. If we are close enough to the edge of
- // the image, curtail the interpolation sources.
- if lowX == highX && lowY == highY {
- p.frac00 = 1.0
- } else if sy-minY <= 0.5 && sx-minX <= 0.5 {
- p.frac00 = 1.0
- } else if maxY-sy <= 0.5 && maxX-sx <= 0.5 {
- p.frac11 = 1.0
- } else if sy-minY <= 0.5 || lowY == highY {
- p.frac00 = x01 - sx
- p.frac01 = sx - x00
- } else if sx-minX <= 0.5 || lowX == highX {
- p.frac00 = y10 - sy
- p.frac10 = sy - y00
- } else if maxY-sy <= 0.5 {
- p.frac10 = x11 - sx
- p.frac11 = sx - x10
- } else if maxX-sx <= 0.5 {
- p.frac01 = y11 - sy
- p.frac11 = sy - y01
- } else {
- p.frac00 = (x01 - sx) * (y10 - sy)
- p.frac01 = (sx - x00) * (y11 - sy)
- p.frac10 = (x11 - sx) * (sy - y00)
- p.frac11 = (sx - x10) * (sy - y01)
- }
-
- return p
-}
-
-// TODO(crawshaw): When we have inlining, consider func (p *RGBA) Off(x, y) int
-func offRGBA(src *image.RGBA, x, y int) int {
- return (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4
-}
-func offGray(src *image.Gray, x, y int) int {
- return (y-src.Rect.Min.Y)*src.Stride + (x - src.Rect.Min.X)
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear_test.go
deleted file mode 100644
index 242d70546..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear_test.go
+++ /dev/null
@@ -1,143 +0,0 @@
-// Copyright 2012 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package interp
-
-import (
- "image"
- "image/color"
- "testing"
-)
-
-type interpTest struct {
- desc string
- src []uint8
- srcWidth int
- x, y float64
- expect uint8
-}
-
-func (p *interpTest) newSrc() *image.RGBA {
- b := image.Rect(0, 0, p.srcWidth, len(p.src)/p.srcWidth)
- src := image.NewRGBA(b)
- i := 0
- for y := b.Min.Y; y < b.Max.Y; y++ {
- for x := b.Min.X; x < b.Max.X; x++ {
- src.SetRGBA(x, y, color.RGBA{
- R: p.src[i],
- G: p.src[i],
- B: p.src[i],
- A: 0xff,
- })
- i++
- }
- }
- return src
-}
-
-var interpTests = []interpTest{
- {
- desc: "center of a single white pixel should match that pixel",
- src: []uint8{0x00},
- srcWidth: 1,
- x: 0.5,
- y: 0.5,
- expect: 0x00,
- },
- {
- desc: "middle of a square is equally weighted",
- src: []uint8{
- 0x00, 0xff,
- 0xff, 0x00,
- },
- srcWidth: 2,
- x: 1.0,
- y: 1.0,
- expect: 0x80,
- },
- {
- desc: "center of a pixel is just that pixel",
- src: []uint8{
- 0x00, 0xff,
- 0xff, 0x00,
- },
- srcWidth: 2,
- x: 1.5,
- y: 0.5,
- expect: 0xff,
- },
- {
- desc: "asymmetry abounds",
- src: []uint8{
- 0xaa, 0x11, 0x55,
- 0xff, 0x95, 0xdd,
- },
- srcWidth: 3,
- x: 2.0,
- y: 1.0,
- expect: 0x76, // (0x11 + 0x55 + 0x95 + 0xdd) / 4
- },
-}
-
-func TestBilinearRGBA(t *testing.T) {
- for _, p := range interpTests {
- src := p.newSrc()
-
- // Fast path.
- c := Bilinear.(RGBA).RGBA(src, p.x, p.y)
- if c.R != c.G || c.R != c.B || c.A != 0xff {
- t.Errorf("expect channels to match, got %v", c)
- continue
- }
- if c.R != p.expect {
- t.Errorf("%s: got 0x%02x want 0x%02x", p.desc, c.R, p.expect)
- continue
- }
-
- // Standard Interp should use the fast path.
- cStd := Bilinear.Interp(src, p.x, p.y)
- if cStd != c {
- t.Errorf("%s: standard mismatch got %v want %v", p.desc, cStd, c)
- continue
- }
-
- // General case should match the fast path.
- cGen := color.RGBAModel.Convert(bilinearGeneral(src, p.x, p.y))
- r0, g0, b0, a0 := c.RGBA()
- r1, g1, b1, a1 := cGen.RGBA()
- if r0 != r1 || g0 != g1 || b0 != b1 || a0 != a1 {
- t.Errorf("%s: general case mismatch got %v want %v", p.desc, c, cGen)
- continue
- }
- }
-}
-
-func TestBilinearSubImage(t *testing.T) {
- b0 := image.Rect(0, 0, 4, 4)
- src0 := image.NewRGBA(b0)
- b1 := image.Rect(1, 1, 3, 3)
- src1 := src0.SubImage(b1).(*image.RGBA)
- src1.Set(1, 1, color.RGBA{0x11, 0, 0, 0xff})
- src1.Set(2, 1, color.RGBA{0x22, 0, 0, 0xff})
- src1.Set(1, 2, color.RGBA{0x33, 0, 0, 0xff})
- src1.Set(2, 2, color.RGBA{0x44, 0, 0, 0xff})
-
- tests := []struct {
- x, y float64
- want uint8
- }{
- {1, 1, 0x11},
- {3, 1, 0x22},
- {1, 3, 0x33},
- {3, 3, 0x44},
- {2, 2, 0x2b},
- }
-
- for _, p := range tests {
- c := Bilinear.(RGBA).RGBA(src1, p.x, p.y)
- if c.R != p.want {
- t.Errorf("(%.0f, %.0f): got 0x%02x want 0x%02x", p.x, p.y, c.R, p.want)
- }
- }
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/doc.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/doc.go
deleted file mode 100644
index b115534cc..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/doc.go
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright 2012 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-/*
-Package interp implements image interpolation.
-
-An interpolator provides the Interp interface, which can be used
-to interpolate a pixel:
-
- c := interp.Bilinear.Interp(src, 1.2, 1.8)
-
-To interpolate a large number of RGBA or Gray pixels, an implementation
-may provide a fast-path by implementing the RGBA or Gray interfaces.
-
- i1, ok := i.(interp.RGBA)
- if ok {
- c := i1.RGBA(src, 1.2, 1.8)
- // use c.R, c.G, etc
- return
- }
- c := i.Interp(src, 1.2, 1.8)
- // use generic color.Color
-*/
-package interp
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/interp.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/interp.go
deleted file mode 100644
index 560637d4a..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/interp.go
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2012 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package interp
-
-import (
- "image"
- "image/color"
-)
-
-// Interp interpolates an image's color at fractional co-ordinates.
-type Interp interface {
- // Interp interpolates (x, y).
- Interp(src image.Image, x, y float64) color.Color
-}
-
-// RGBA is a fast-path interpolation implementation for image.RGBA.
-// It is common for an Interp to also implement RGBA.
-type RGBA interface {
- // RGBA interpolates (x, y).
- RGBA(src *image.RGBA, x, y float64) color.RGBA
-}
-
-// Gray is a fast-path interpolation implementation for image.Gray.
-type Gray interface {
- // Gray interpolates (x, y).
- Gray(src *image.Gray, x, y float64) color.Gray
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate.go
deleted file mode 100644
index 62bde1a08..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "code.google.com/p/graphics-go/graphics/interp"
- "errors"
- "image"
- "image/draw"
-)
-
-// RotateOptions are the rotation parameters.
-// Angle is the angle, in radians, to rotate the image clockwise.
-type RotateOptions struct {
- Angle float64
-}
-
-// Rotate produces a rotated version of src, drawn onto dst.
-func Rotate(dst draw.Image, src image.Image, opt *RotateOptions) error {
- if dst == nil {
- return errors.New("graphics: dst is nil")
- }
- if src == nil {
- return errors.New("graphics: src is nil")
- }
-
- angle := 0.0
- if opt != nil {
- angle = opt.Angle
- }
-
- return I.Rotate(angle).TransformCenter(dst, src, interp.Bilinear)
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate_test.go
deleted file mode 100644
index bfc532a0a..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate_test.go
+++ /dev/null
@@ -1,169 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "code.google.com/p/graphics-go/graphics/graphicstest"
- "image"
- "math"
- "testing"
-
- _ "image/png"
-)
-
-var rotateOneColorTests = []transformOneColorTest{
- {
- "onepixel-onequarter", 1, 1, 1, 1,
- &RotateOptions{math.Pi / 2},
- []uint8{0xff},
- []uint8{0xff},
- },
- {
- "onepixel-partial", 1, 1, 1, 1,
- &RotateOptions{math.Pi * 2.0 / 3.0},
- []uint8{0xff},
- []uint8{0xff},
- },
- {
- "onepixel-complete", 1, 1, 1, 1,
- &RotateOptions{2 * math.Pi},
- []uint8{0xff},
- []uint8{0xff},
- },
- {
- "even-onequarter", 2, 2, 2, 2,
- &RotateOptions{math.Pi / 2.0},
- []uint8{
- 0xff, 0x00,
- 0x00, 0xff,
- },
- []uint8{
- 0x00, 0xff,
- 0xff, 0x00,
- },
- },
- {
- "even-complete", 2, 2, 2, 2,
- &RotateOptions{2.0 * math.Pi},
- []uint8{
- 0xff, 0x00,
- 0x00, 0xff,
- },
- []uint8{
- 0xff, 0x00,
- 0x00, 0xff,
- },
- },
- {
- "line-partial", 3, 3, 3, 3,
- &RotateOptions{math.Pi * 1.0 / 3.0},
- []uint8{
- 0x00, 0x00, 0x00,
- 0xff, 0xff, 0xff,
- 0x00, 0x00, 0x00,
- },
- []uint8{
- 0xa2, 0x80, 0x00,
- 0x22, 0xff, 0x22,
- 0x00, 0x80, 0xa2,
- },
- },
- {
- "line-offset-partial", 3, 3, 3, 3,
- &RotateOptions{math.Pi * 3 / 2},
- []uint8{
- 0x00, 0x00, 0x00,
- 0x00, 0xff, 0xff,
- 0x00, 0x00, 0x00,
- },
- []uint8{
- 0x00, 0xff, 0x00,
- 0x00, 0xff, 0x00,
- 0x00, 0x00, 0x00,
- },
- },
- {
- "dot-partial", 4, 4, 4, 4,
- &RotateOptions{math.Pi},
- []uint8{
- 0x00, 0x00, 0x00, 0x00,
- 0x00, 0xff, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00,
- },
- []uint8{
- 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0xff, 0x00,
- 0x00, 0x00, 0x00, 0x00,
- },
- },
-}
-
-func TestRotateOneColor(t *testing.T) {
- for _, oc := range rotateOneColorTests {
- src := oc.newSrc()
- dst := oc.newDst()
-
- if err := Rotate(dst, src, oc.opt.(*RotateOptions)); err != nil {
- t.Errorf("rotate %s: %v", oc.desc, err)
- continue
- }
- if !checkTransformTest(t, &oc, dst) {
- continue
- }
- }
-}
-
-func TestRotateEmpty(t *testing.T) {
- empty := image.NewRGBA(image.Rect(0, 0, 0, 0))
- if err := Rotate(empty, empty, nil); err != nil {
- t.Fatal(err)
- }
-}
-
-func TestRotateGopherSide(t *testing.T) {
- src, err := graphicstest.LoadImage("../testdata/gopher.png")
- if err != nil {
- t.Fatal(err)
- }
-
- srcb := src.Bounds()
- dst := image.NewRGBA(image.Rect(0, 0, srcb.Dy(), srcb.Dx()))
- if err := Rotate(dst, src, &RotateOptions{math.Pi / 2.0}); err != nil {
- t.Fatal(err)
- }
-
- cmp, err := graphicstest.LoadImage("../testdata/gopher-rotate-side.png")
- if err != nil {
- t.Fatal(err)
- }
- err = graphicstest.ImageWithinTolerance(dst, cmp, 0x101)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func TestRotateGopherPartial(t *testing.T) {
- src, err := graphicstest.LoadImage("../testdata/gopher.png")
- if err != nil {
- t.Fatal(err)
- }
-
- srcb := src.Bounds()
- dst := image.NewRGBA(image.Rect(0, 0, srcb.Dx(), srcb.Dy()))
- if err := Rotate(dst, src, &RotateOptions{math.Pi / 3.0}); err != nil {
- t.Fatal(err)
- }
-
- cmp, err := graphicstest.LoadImage("../testdata/gopher-rotate-partial.png")
- if err != nil {
- t.Fatal(err)
- }
- err = graphicstest.ImageWithinTolerance(dst, cmp, 0x101)
- if err != nil {
- t.Fatal(err)
- }
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale.go
deleted file mode 100644
index 7a7fe9696..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale.go
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "code.google.com/p/graphics-go/graphics/interp"
- "errors"
- "image"
- "image/draw"
-)
-
-// Scale produces a scaled version of the image using bilinear interpolation.
-func Scale(dst draw.Image, src image.Image) error {
- if dst == nil {
- return errors.New("graphics: dst is nil")
- }
- if src == nil {
- return errors.New("graphics: src is nil")
- }
-
- b := dst.Bounds()
- srcb := src.Bounds()
- if b.Empty() || srcb.Empty() {
- return nil
- }
- sx := float64(b.Dx()) / float64(srcb.Dx())
- sy := float64(b.Dy()) / float64(srcb.Dy())
- return I.Scale(sx, sy).Transform(dst, src, interp.Bilinear)
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale_test.go
deleted file mode 100644
index 9c2468f11..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale_test.go
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "code.google.com/p/graphics-go/graphics/graphicstest"
- "image"
- "testing"
-
- _ "image/png"
-)
-
-var scaleOneColorTests = []transformOneColorTest{
- {
- "down-half",
- 1, 1,
- 2, 2,
- nil,
- []uint8{
- 0x80, 0x00,
- 0x00, 0x80,
- },
- []uint8{
- 0x40,
- },
- },
- {
- "up-double",
- 4, 4,
- 2, 2,
- nil,
- []uint8{
- 0x80, 0x00,
- 0x00, 0x80,
- },
- []uint8{
- 0x80, 0x60, 0x20, 0x00,
- 0x60, 0x50, 0x30, 0x20,
- 0x20, 0x30, 0x50, 0x60,
- 0x00, 0x20, 0x60, 0x80,
- },
- },
- {
- "up-doublewidth",
- 4, 2,
- 2, 2,
- nil,
- []uint8{
- 0x80, 0x00,
- 0x00, 0x80,
- },
- []uint8{
- 0x80, 0x60, 0x20, 0x00,
- 0x00, 0x20, 0x60, 0x80,
- },
- },
- {
- "up-doubleheight",
- 2, 4,
- 2, 2,
- nil,
- []uint8{
- 0x80, 0x00,
- 0x00, 0x80,
- },
- []uint8{
- 0x80, 0x00,
- 0x60, 0x20,
- 0x20, 0x60,
- 0x00, 0x80,
- },
- },
- {
- "up-partial",
- 3, 3,
- 2, 2,
- nil,
- []uint8{
- 0x80, 0x00,
- 0x00, 0x80,
- },
- []uint8{
- 0x80, 0x40, 0x00,
- 0x40, 0x40, 0x40,
- 0x00, 0x40, 0x80,
- },
- },
-}
-
-func TestScaleOneColor(t *testing.T) {
- for _, oc := range scaleOneColorTests {
- dst := oc.newDst()
- src := oc.newSrc()
- if err := Scale(dst, src); err != nil {
- t.Errorf("scale %s: %v", oc.desc, err)
- continue
- }
-
- if !checkTransformTest(t, &oc, dst) {
- continue
- }
- }
-}
-
-func TestScaleEmpty(t *testing.T) {
- empty := image.NewRGBA(image.Rect(0, 0, 0, 0))
- if err := Scale(empty, empty); err != nil {
- t.Fatal(err)
- }
-}
-
-func TestScaleGopher(t *testing.T) {
- dst := image.NewRGBA(image.Rect(0, 0, 100, 150))
-
- src, err := graphicstest.LoadImage("../testdata/gopher.png")
- if err != nil {
- t.Error(err)
- return
- }
-
- // Down-sample.
- if err := Scale(dst, src); err != nil {
- t.Fatal(err)
- }
- cmp, err := graphicstest.LoadImage("../testdata/gopher-100x150.png")
- if err != nil {
- t.Error(err)
- return
- }
- err = graphicstest.ImageWithinTolerance(dst, cmp, 0)
- if err != nil {
- t.Error(err)
- return
- }
-
- // Up-sample.
- dst = image.NewRGBA(image.Rect(0, 0, 500, 750))
- if err := Scale(dst, src); err != nil {
- t.Fatal(err)
- }
- cmp, err = graphicstest.LoadImage("../testdata/gopher-500x750.png")
- if err != nil {
- t.Error(err)
- return
- }
- err = graphicstest.ImageWithinTolerance(dst, cmp, 0)
- if err != nil {
- t.Error(err)
- return
- }
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/shared_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/shared_test.go
deleted file mode 100644
index e1cd21fb3..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/shared_test.go
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "bytes"
- "code.google.com/p/graphics-go/graphics/graphicstest"
- "image"
- "image/color"
- "testing"
-)
-
-type transformOneColorTest struct {
- desc string
- dstWidth int
- dstHeight int
- srcWidth int
- srcHeight int
- opt interface{}
- src []uint8
- res []uint8
-}
-
-func (oc *transformOneColorTest) newSrc() *image.RGBA {
- b := image.Rect(0, 0, oc.srcWidth, oc.srcHeight)
- src := image.NewRGBA(b)
- i := 0
- for y := b.Min.Y; y < b.Max.Y; y++ {
- for x := b.Min.X; x < b.Max.X; x++ {
- src.SetRGBA(x, y, color.RGBA{
- R: oc.src[i],
- G: oc.src[i],
- B: oc.src[i],
- A: oc.src[i],
- })
- i++
- }
- }
- return src
-}
-
-func (oc *transformOneColorTest) newDst() *image.RGBA {
- return image.NewRGBA(image.Rect(0, 0, oc.dstWidth, oc.dstHeight))
-}
-
-func checkTransformTest(t *testing.T, oc *transformOneColorTest, dst *image.RGBA) bool {
- for ch := 0; ch < 4; ch++ {
- i := 0
- res := make([]byte, len(oc.res))
- for y := 0; y < oc.dstHeight; y++ {
- for x := 0; x < oc.dstWidth; x++ {
- off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
- res[i] = dst.Pix[off+ch]
- i++
- }
- }
-
- if !bytes.Equal(res, oc.res) {
- got := graphicstest.SprintBox(res, oc.dstWidth, oc.dstHeight)
- want := graphicstest.SprintBox(oc.res, oc.dstWidth, oc.dstHeight)
- t.Errorf("%s: ch=%d\n got\n%s\n want\n%s", oc.desc, ch, got, want)
- return false
- }
- }
-
- return true
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail.go
deleted file mode 100644
index d3ad7e8f7..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail.go
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "image"
- "image/draw"
-)
-
-// Thumbnail scales and crops src so it fits in dst.
-func Thumbnail(dst draw.Image, src image.Image) error {
- // Scale down src in the dimension that is closer to dst.
- sb := src.Bounds()
- db := dst.Bounds()
- rx := float64(sb.Dx()) / float64(db.Dx())
- ry := float64(sb.Dy()) / float64(db.Dy())
- var b image.Rectangle
- if rx < ry {
- b = image.Rect(0, 0, db.Dx(), int(float64(sb.Dy())/rx))
- } else {
- b = image.Rect(0, 0, int(float64(sb.Dx())/ry), db.Dy())
- }
-
- buf := image.NewRGBA(b)
- if err := Scale(buf, src); err != nil {
- return err
- }
-
- // Crop.
- // TODO(crawshaw): improve on center-alignment.
- var pt image.Point
- if rx < ry {
- pt.Y = (b.Dy() - db.Dy()) / 2
- } else {
- pt.X = (b.Dx() - db.Dx()) / 2
- }
- draw.Draw(dst, db, buf, pt, draw.Src)
- return nil
-}
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail_test.go
deleted file mode 100644
index d12659f17..000000000
--- a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright 2011 The Graphics-Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package graphics
-
-import (
- "code.google.com/p/graphics-go/graphics/graphicstest"
- "image"
- "testing"
-
- _ "image/png"
-)
-
-func TestThumbnailGopher(t *testing.T) {
- dst := image.NewRGBA(image.Rect(0, 0, 80, 80))
-
- src, err := graphicstest.LoadImage("../testdata/gopher.png")
- if err != nil {
- t.Fatal(err)
- }
- if err := Thumbnail(dst, src); err != nil {
- t.Fatal(err)
- }
- cmp, err := graphicstest.LoadImage("../testdata/gopher-thumb-80x80.png")
- if err != nil {
- t.Fatal(err)
- }
- err = graphicstest.ImageWithinTolerance(dst, cmp, 0)
- if err != nil {
- t.Error(err)
- }
-}
-
-func TestThumbnailLongGopher(t *testing.T) {
- dst := image.NewRGBA(image.Rect(0, 0, 50, 150))
-
- src, err := graphicstest.LoadImage("../testdata/gopher.png")
- if err != nil {
- t.Fatal(err)
- }
- if err := Thumbnail(dst, src); err != nil {
- t.Fatal(err)
- }
- cmp, err := graphicstest.LoadImage("../testdata/gopher-thumb-50x150.png")
- if err != nil {
- t.Fatal(err)
- }
- err = graphicstest.ImageWithinTolerance(dst, cmp, 0)
- if err != nil {
- t.Error(err)
- }
-}
diff --git a/Godeps/_workspace/src/github.com/disintegration/imaging/LICENSE b/Godeps/_workspace/src/github.com/disintegration/imaging/LICENSE
new file mode 100644
index 000000000..95ae410c3
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/disintegration/imaging/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2012-2014 Grigory Dryapak
+
+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. \ No newline at end of file
diff --git a/Godeps/_workspace/src/github.com/disintegration/imaging/README.md b/Godeps/_workspace/src/github.com/disintegration/imaging/README.md
new file mode 100644
index 000000000..16ac8cf6c
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/disintegration/imaging/README.md
@@ -0,0 +1,160 @@
+# Imaging
+
+Package imaging provides basic image manipulation functions (resize, rotate, flip, crop, etc.).
+This package is based on the standard Go image package and works best along with it.
+
+Image manipulation functions provided by the package take any image type
+that implements `image.Image` interface as an input, and return a new image of
+`*image.NRGBA` type (32bit RGBA colors, not premultiplied by alpha).
+
+## Installation
+
+Imaging requires Go version 1.2 or greater.
+
+ go get -u github.com/disintegration/imaging
+
+## Documentation
+
+http://godoc.org/github.com/disintegration/imaging
+
+## Usage examples
+
+A few usage examples can be found below. See the documentation for the full list of supported functions.
+
+### Image resizing
+```go
+// resize srcImage to size = 128x128px using the Lanczos filter
+dstImage128 := imaging.Resize(srcImage, 128, 128, imaging.Lanczos)
+
+// resize srcImage to width = 800px preserving the aspect ratio
+dstImage800 := imaging.Resize(srcImage, 800, 0, imaging.Lanczos)
+
+// scale down srcImage to fit the 800x600px bounding box
+dstImageFit := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
+
+// resize and crop the srcImage to make a 100x100px thumbnail
+dstImageThumb := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
+```
+
+Imaging supports image resizing using various resampling filters. The most notable ones:
+- `NearestNeighbor` - Fastest resampling filter, no antialiasing.
+- `Box` - Simple and fast averaging filter appropriate for downscaling. When upscaling it's similar to NearestNeighbor.
+- `Linear` - Bilinear filter, smooth and reasonably fast.
+- `MitchellNetravali` - А smooth bicubic filter.
+- `CatmullRom` - A sharp bicubic filter.
+- `Gaussian` - Blurring filter that uses gaussian function, useful for noise removal.
+- `Lanczos` - High-quality resampling filter for photographic images yielding sharp results, but it's slower than cubic filters.
+
+The full list of supported filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali, CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine. Custom filters can be created using ResampleFilter struct.
+
+**Resampling filters comparison**
+
+Original image. Will be resized from 512x512px to 128x128px.
+
+![srcImage](http://disintegration.github.io/imaging/in_lena_bw_512.png)
+
+Filter | Resize result
+---|---
+`imaging.NearestNeighbor` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_nearest.png)
+`imaging.Box` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_box.png)
+`imaging.Linear` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_linear.png)
+`imaging.MitchellNetravali` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_mitchell.png)
+`imaging.CatmullRom` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_catrom.png)
+`imaging.Gaussian` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_gaussian.png)
+`imaging.Lanczos` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_lanczos.png)
+
+### Gaussian Blur
+```go
+dstImage := imaging.Blur(srcImage, 0.5)
+```
+
+Sigma parameter allows to control the strength of the blurring effect.
+
+Original image | Sigma = 0.5 | Sigma = 1.5
+---|---|---
+![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_blur_0.5.png) | ![dstImage](http://disintegration.github.io/imaging/out_blur_1.5.png)
+
+### Sharpening
+```go
+dstImage := imaging.Sharpen(srcImage, 0.5)
+```
+
+Uses gaussian function internally. Sigma parameter allows to control the strength of the sharpening effect.
+
+Original image | Sigma = 0.5 | Sigma = 1.5
+---|---|---
+![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_sharpen_0.5.png) | ![dstImage](http://disintegration.github.io/imaging/out_sharpen_1.5.png)
+
+### Gamma correction
+```go
+dstImage := imaging.AdjustGamma(srcImage, 0.75)
+```
+
+Original image | Gamma = 0.75 | Gamma = 1.25
+---|---|---
+![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_gamma_0.75.png) | ![dstImage](http://disintegration.github.io/imaging/out_gamma_1.25.png)
+
+### Contrast adjustment
+```go
+dstImage := imaging.AdjustContrast(srcImage, 20)
+```
+
+Original image | Contrast = 20 | Contrast = -20
+---|---|---
+![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_contrast_p20.png) | ![dstImage](http://disintegration.github.io/imaging/out_contrast_m20.png)
+
+### Brightness adjustment
+```go
+dstImage := imaging.AdjustBrightness(srcImage, 20)
+```
+
+Original image | Brightness = 20 | Brightness = -20
+---|---|---
+![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_brightness_p20.png) | ![dstImage](http://disintegration.github.io/imaging/out_brightness_m20.png)
+
+
+### Complete code example
+Here is the code example that loads several images, makes thumbnails of them
+and combines them together side-by-side.
+
+```go
+package main
+
+import (
+ "image"
+ "image/color"
+
+ "github.com/disintegration/imaging"
+)
+
+func main() {
+
+ // input files
+ files := []string{"01.jpg", "02.jpg", "03.jpg"}
+
+ // load images and make 100x100 thumbnails of them
+ var thumbnails []image.Image
+ for _, file := range files {
+ img, err := imaging.Open(file)
+ if err != nil {
+ panic(err)
+ }
+ thumb := imaging.Thumbnail(img, 100, 100, imaging.CatmullRom)
+ thumbnails = append(thumbnails, thumb)
+ }
+
+ // create a new blank image
+ dst := imaging.New(100*len(thumbnails), 100, color.NRGBA{0, 0, 0, 0})
+
+ // paste thumbnails into the new image side by side
+ for i, thumb := range thumbnails {
+ dst = imaging.Paste(dst, thumb, image.Pt(i*100, 0))
+ }
+
+ // save the combined image to file
+ err := imaging.Save(dst, "dst.jpg")
+ if err != nil {
+ panic(err)
+ }
+}
+```
diff --git a/Godeps/_workspace/src/github.com/disintegration/imaging/adjust.go b/Godeps/_workspace/src/github.com/disintegration/imaging/adjust.go
new file mode 100644
index 000000000..9b1b83a4f
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/disintegration/imaging/adjust.go
@@ -0,0 +1,200 @@
+package imaging
+
+import (
+ "image"
+ "image/color"
+ "math"
+)
+
+// AdjustFunc applies the fn function to each pixel of the img image and returns the adjusted image.
+//
+// Example:
+//
+// dstImage = imaging.AdjustFunc(
+// srcImage,
+// func(c color.NRGBA) color.NRGBA {
+// // shift the red channel by 16
+// r := int(c.R) + 16
+// if r > 255 {
+// r = 255
+// }
+// return color.NRGBA{uint8(r), c.G, c.B, c.A}
+// }
+// )
+//
+func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA {
+ src := toNRGBA(img)
+ width := src.Bounds().Max.X
+ height := src.Bounds().Max.Y
+ dst := image.NewNRGBA(image.Rect(0, 0, width, height))
+
+ parallel(height, func(partStart, partEnd int) {
+ for y := partStart; y < partEnd; y++ {
+ for x := 0; x < width; x++ {
+ i := y*src.Stride + x*4
+ j := y*dst.Stride + x*4
+
+ r := src.Pix[i+0]
+ g := src.Pix[i+1]
+ b := src.Pix[i+2]
+ a := src.Pix[i+3]
+
+ c := fn(color.NRGBA{r, g, b, a})
+
+ dst.Pix[j+0] = c.R
+ dst.Pix[j+1] = c.G
+ dst.Pix[j+2] = c.B
+ dst.Pix[j+3] = c.A
+ }
+ }
+ })
+
+ return dst
+}
+
+// AdjustGamma performs a gamma correction on the image and returns the adjusted image.
+// Gamma parameter must be positive. Gamma = 1.0 gives the original image.
+// Gamma less than 1.0 darkens the image and gamma greater than 1.0 lightens it.
+//
+// Example:
+//
+// dstImage = imaging.AdjustGamma(srcImage, 0.7)
+//
+func AdjustGamma(img image.Image, gamma float64) *image.NRGBA {
+ e := 1.0 / math.Max(gamma, 0.0001)
+ lut := make([]uint8, 256)
+
+ for i := 0; i < 256; i++ {
+ lut[i] = clamp(math.Pow(float64(i)/255.0, e) * 255.0)
+ }
+
+ fn := func(c color.NRGBA) color.NRGBA {
+ return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
+ }
+
+ return AdjustFunc(img, fn)
+}
+
+func sigmoid(a, b, x float64) float64 {
+ return 1 / (1 + math.Exp(b*(a-x)))
+}
+
+// AdjustSigmoid changes the contrast of the image using a sigmoidal function and returns the adjusted image.
+// It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
+// The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5.
+// The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10).
+// If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased.
+//
+// Examples:
+//
+// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, 3.0) // increase the contrast
+// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, -3.0) // decrease the contrast
+//
+func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA {
+ if factor == 0 {
+ return Clone(img)
+ }
+
+ lut := make([]uint8, 256)
+ a := math.Min(math.Max(midpoint, 0.0), 1.0)
+ b := math.Abs(factor)
+ sig0 := sigmoid(a, b, 0)
+ sig1 := sigmoid(a, b, 1)
+ e := 1.0e-6
+
+ if factor > 0 {
+ for i := 0; i < 256; i++ {
+ x := float64(i) / 255.0
+ sigX := sigmoid(a, b, x)
+ f := (sigX - sig0) / (sig1 - sig0)
+ lut[i] = clamp(f * 255.0)
+ }
+ } else {
+ for i := 0; i < 256; i++ {
+ x := float64(i) / 255.0
+ arg := math.Min(math.Max((sig1-sig0)*x+sig0, e), 1.0-e)
+ f := a - math.Log(1.0/arg-1.0)/b
+ lut[i] = clamp(f * 255.0)
+ }
+ }
+
+ fn := func(c color.NRGBA) color.NRGBA {
+ return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
+ }
+
+ return AdjustFunc(img, fn)
+}
+
+// AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
+// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
+// The percentage = -100 gives solid grey image.
+//
+// Examples:
+//
+// dstImage = imaging.AdjustContrast(srcImage, -10) // decrease image contrast by 10%
+// dstImage = imaging.AdjustContrast(srcImage, 20) // increase image contrast by 20%
+//
+func AdjustContrast(img image.Image, percentage float64) *image.NRGBA {
+ percentage = math.Min(math.Max(percentage, -100.0), 100.0)
+ lut := make([]uint8, 256)
+
+ v := (100.0 + percentage) / 100.0
+ for i := 0; i < 256; i++ {
+ if 0 <= v && v <= 1 {
+ lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*v) * 255.0)
+ } else if 1 < v && v < 2 {
+ lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*(1/(2.0-v))) * 255.0)
+ } else {
+ lut[i] = uint8(float64(i)/255.0+0.5) * 255
+ }
+ }
+
+ fn := func(c color.NRGBA) color.NRGBA {
+ return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
+ }
+
+ return AdjustFunc(img, fn)
+}
+
+// AdjustBrightness changes the brightness of the image using the percentage parameter and returns the adjusted image.
+// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
+// The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
+//
+// Examples:
+//
+// dstImage = imaging.AdjustBrightness(srcImage, -15) // decrease image brightness by 15%
+// dstImage = imaging.AdjustBrightness(srcImage, 10) // increase image brightness by 10%
+//
+func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA {
+ percentage = math.Min(math.Max(percentage, -100.0), 100.0)
+ lut := make([]uint8, 256)
+
+ shift := 255.0 * percentage / 100.0
+ for i := 0; i < 256; i++ {
+ lut[i] = clamp(float64(i) + shift)
+ }
+
+ fn := func(c color.NRGBA) color.NRGBA {
+ return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
+ }
+
+ return AdjustFunc(img, fn)
+}
+
+// Grayscale produces grayscale version of the image.
+func Grayscale(img image.Image) *image.NRGBA {
+ fn := func(c color.NRGBA) color.NRGBA {
+ f := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
+ y := uint8(f + 0.5)
+ return color.NRGBA{y, y, y, c.A}
+ }
+ return AdjustFunc(img, fn)
+}
+
+// Invert produces inverted (negated) version of the image.
+func Invert(img image.Image) *image.NRGBA {
+ fn := func(c color.NRGBA) color.NRGBA {
+ return color.NRGBA{255 - c.R, 255 - c.G, 255 - c.B, c.A}
+ }
+ return AdjustFunc(img, fn)
+}
diff --git a/Godeps/_workspace/src/github.com/disintegration/imaging/effects.go b/Godeps/_workspace/src/github.com/disintegration/imaging/effects.go
new file mode 100644
index 000000000..fe92e10a2
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/disintegration/imaging/effects.go
@@ -0,0 +1,187 @@
+package imaging
+
+import (
+ "image"
+ "math"
+)
+
+func gaussianBlurKernel(x, sigma float64) float64 {
+ return math.Exp(-(x*x)/(2*sigma*sigma)) / (sigma * math.Sqrt(2*math.Pi))
+}
+
+// Blur produces a blurred version of the image using a Gaussian function.
+// Sigma parameter must be positive and indicates how much the image will be blurred.
+//
+// Usage example:
+//
+// dstImage := imaging.Blur(srcImage, 3.5)
+//
+func Blur(img image.Image, sigma float64) *image.NRGBA {
+ if sigma <= 0 {
+ // sigma parameter must be positive!
+ return Clone(img)
+ }
+
+ src := toNRGBA(img)
+ radius := int(math.Ceil(sigma * 3.0))
+ kernel := make([]float64, radius+1)
+
+ for i := 0; i <= radius; i++ {
+ kernel[i] = gaussianBlurKernel(float64(i), sigma)
+ }
+
+ var dst *image.NRGBA
+ dst = blurHorizontal(src, kernel)
+ dst = blurVertical(dst, kernel)
+
+ return dst
+}
+
+func blurHorizontal(src *image.NRGBA, kernel []float64) *image.NRGBA {
+ radius := len(kernel) - 1
+ width := src.Bounds().Max.X
+ height := src.Bounds().Max.Y
+
+ dst := image.NewNRGBA(image.Rect(0, 0, width, height))
+
+ parallel(width, func(partStart, partEnd int) {
+ for x := partStart; x < partEnd; x++ {
+ start := x - radius
+ if start < 0 {
+ start = 0
+ }
+
+ end := x + radius
+ if end > width-1 {
+ end = width - 1
+ }
+
+ weightSum := 0.0
+ for ix := start; ix <= end; ix++ {
+ weightSum += kernel[absint(x-ix)]
+ }
+
+ for y := 0; y < height; y++ {
+
+ r, g, b, a := 0.0, 0.0, 0.0, 0.0
+ for ix := start; ix <= end; ix++ {
+ weight := kernel[absint(x-ix)]
+ i := y*src.Stride + ix*4
+ r += float64(src.Pix[i+0]) * weight
+ g += float64(src.Pix[i+1]) * weight
+ b += float64(src.Pix[i+2]) * weight
+ a += float64(src.Pix[i+3]) * weight
+ }
+
+ r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
+ g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
+ b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
+ a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
+
+ j := y*dst.Stride + x*4
+ dst.Pix[j+0] = uint8(r + 0.5)
+ dst.Pix[j+1] = uint8(g + 0.5)
+ dst.Pix[j+2] = uint8(b + 0.5)
+ dst.Pix[j+3] = uint8(a + 0.5)
+
+ }
+ }
+ })
+
+ return dst
+}
+
+func blurVertical(src *image.NRGBA, kernel []float64) *image.NRGBA {
+ radius := len(kernel) - 1
+ width := src.Bounds().Max.X
+ height := src.Bounds().Max.Y
+
+ dst := image.NewNRGBA(image.Rect(0, 0, width, height))
+
+ parallel(height, func(partStart, partEnd int) {
+ for y := partStart; y < partEnd; y++ {
+ start := y - radius
+ if start < 0 {
+ start = 0
+ }
+
+ end := y + radius
+ if end > height-1 {
+ end = height - 1
+ }
+
+ weightSum := 0.0
+ for iy := start; iy <= end; iy++ {
+ weightSum += kernel[absint(y-iy)]
+ }
+
+ for x := 0; x < width; x++ {
+
+ r, g, b, a := 0.0, 0.0, 0.0, 0.0
+ for iy := start; iy <= end; iy++ {
+ weight := kernel[absint(y-iy)]
+ i := iy*src.Stride + x*4
+ r += float64(src.Pix[i+0]) * weight
+ g += float64(src.Pix[i+1]) * weight
+ b += float64(src.Pix[i+2]) * weight
+ a += float64(src.Pix[i+3]) * weight
+ }
+
+ r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
+ g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
+ b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
+ a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
+
+ j := y*dst.Stride + x*4
+ dst.Pix[j+0] = uint8(r + 0.5)
+ dst.Pix[j+1] = uint8(g + 0.5)
+ dst.Pix[j+2] = uint8(b + 0.5)
+ dst.Pix[j+3] = uint8(a + 0.5)
+
+ }
+ }
+ })
+
+ return dst
+}
+
+// Sharpen produces a sharpened version of the image.
+// Sigma parameter must be positive and indicates how much the image will be sharpened.
+//
+// Usage example:
+//
+// dstImage := imaging.Sharpen(srcImage, 3.5)
+//
+func Sharpen(img image.Image, sigma float64) *image.NRGBA {
+ if sigma <= 0 {
+ // sigma parameter must be positive!
+ return Clone(img)
+ }
+
+ src := toNRGBA(img)
+ blurred := Blur(img, sigma)
+
+ width := src.Bounds().Max.X
+ height := src.Bounds().Max.Y
+ dst := image.NewNRGBA(image.Rect(0, 0, width, height))
+
+ parallel(height, func(partStart, partEnd int) {
+ for y := partStart; y < partEnd; y++ {
+ for x := 0; x < width; x++ {
+ i := y*src.Stride + x*4
+ for j := 0; j < 4; j++ {
+ k := i + j
+ val := int(src.Pix[k]) + (int(src.Pix[k]) - int(blurred.Pix[k]))
+ if val < 0 {
+ val = 0
+ } else if val > 255 {
+ val = 255
+ }
+ dst.Pix[k] = uint8(val)
+ }
+ }
+ }
+ })
+
+ return dst
+}
diff --git a/Godeps/_workspace/src/github.com/disintegration/imaging/helpers.go b/Godeps/_workspace/src/github.com/disintegration/imaging/helpers.go
new file mode 100644
index 000000000..983b64d71
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/disintegration/imaging/helpers.go
@@ -0,0 +1,392 @@
+/*
+Package imaging provides basic image manipulation functions (resize, rotate, flip, crop, etc.).
+This package is based on the standard Go image package and works best along with it.
+
+Image manipulation functions provided by the package take any image type
+that implements `image.Image` interface as an input, and return a new image of
+`*image.NRGBA` type (32bit RGBA colors, not premultiplied by alpha).
+*/
+package imaging
+
+import (
+ "errors"
+ "image"
+ "image/color"
+ "image/gif"
+ "image/jpeg"
+ "image/png"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/image/bmp"
+ "golang.org/x/image/tiff"
+)
+
+type Format int
+
+const (
+ JPEG Format = iota
+ PNG
+ GIF
+ TIFF
+ BMP
+)
+
+func (f Format) String() string {
+ switch f {
+ case JPEG:
+ return "JPEG"
+ case PNG:
+ return "PNG"
+ case GIF:
+ return "GIF"
+ case TIFF:
+ return "TIFF"
+ case BMP:
+ return "BMP"
+ default:
+ return "Unsupported"
+ }
+}
+
+var (
+ ErrUnsupportedFormat = errors.New("imaging: unsupported image format")
+)
+
+// Decode reads an image from r.
+func Decode(r io.Reader) (image.Image, error) {
+ img, _, err := image.Decode(r)
+ if err != nil {
+ return nil, err
+ }
+ return toNRGBA(img), nil
+}
+
+// Open loads an image from file
+func Open(filename string) (image.Image, error) {
+ file, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ img, err := Decode(file)
+ return img, err
+}
+
+// Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP).
+func Encode(w io.Writer, img image.Image, format Format) error {
+ var err error
+ switch format {
+ case JPEG:
+ var rgba *image.RGBA
+ if nrgba, ok := img.(*image.NRGBA); ok {
+ if nrgba.Opaque() {
+ rgba = &image.RGBA{
+ Pix: nrgba.Pix,
+ Stride: nrgba.Stride,
+ Rect: nrgba.Rect,
+ }
+ }
+ }
+ if rgba != nil {
+ err = jpeg.Encode(w, rgba, &jpeg.Options{Quality: 95})
+ } else {
+ err = jpeg.Encode(w, img, &jpeg.Options{Quality: 95})
+ }
+
+ case PNG:
+ err = png.Encode(w, img)
+ case GIF:
+ err = gif.Encode(w, img, &gif.Options{NumColors: 256})
+ case TIFF:
+ err = tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
+ case BMP:
+ err = bmp.Encode(w, img)
+ default:
+ err = ErrUnsupportedFormat
+ }
+ return err
+}
+
+// Save saves the image to file with the specified filename.
+// The format is determined from the filename extension: "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
+func Save(img image.Image, filename string) (err error) {
+ formats := map[string]Format{
+ ".jpg": JPEG,
+ ".jpeg": JPEG,
+ ".png": PNG,
+ ".tif": TIFF,
+ ".tiff": TIFF,
+ ".bmp": BMP,
+ ".gif": GIF,
+ }
+
+ ext := strings.ToLower(filepath.Ext(filename))
+ f, ok := formats[ext]
+ if !ok {
+ return ErrUnsupportedFormat
+ }
+
+ file, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ return Encode(file, img, f)
+}
+
+// New creates a new image with the specified width and height, and fills it with the specified color.
+func New(width, height int, fillColor color.Color) *image.NRGBA {
+ if width <= 0 || height <= 0 {
+ return &image.NRGBA{}
+ }
+
+ dst := image.NewNRGBA(image.Rect(0, 0, width, height))
+ c := color.NRGBAModel.Convert(fillColor).(color.NRGBA)
+
+ if c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0 {
+ return dst
+ }
+
+ cs := []uint8{c.R, c.G, c.B, c.A}
+
+ // fill the first row
+ for x := 0; x < width; x++ {
+ copy(dst.Pix[x*4:(x+1)*4], cs)
+ }
+ // copy the first row to other rows
+ for y := 1; y < height; y++ {
+ copy(dst.Pix[y*dst.Stride:y*dst.Stride+width*4], dst.Pix[0:width*4])
+ }
+
+ return dst
+}
+
+// Clone returns a copy of the given image.
+func Clone(img image.Image) *image.NRGBA {
+ srcBounds := img.Bounds()
+ srcMinX := srcBounds.Min.X
+ srcMinY := srcBounds.Min.Y
+
+ dstBounds := srcBounds.Sub(srcBounds.Min)
+ dstW := dstBounds.Dx()
+ dstH := dstBounds.Dy()
+ dst := image.NewNRGBA(dstBounds)
+
+ switch src := img.(type) {
+
+ case *image.NRGBA:
+ rowSize := srcBounds.Dx() * 4
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ di := dst.PixOffset(0, dstY)
+ si := src.PixOffset(srcMinX, srcMinY+dstY)
+ copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize])
+ }
+ })
+
+ case *image.NRGBA64:
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ di := dst.PixOffset(0, dstY)
+ si := src.PixOffset(srcMinX, srcMinY+dstY)
+ for dstX := 0; dstX < dstW; dstX++ {
+
+ dst.Pix[di+0] = src.Pix[si+0]
+ dst.Pix[di+1] = src.Pix[si+2]
+ dst.Pix[di+2] = src.Pix[si+4]
+ dst.Pix[di+3] = src.Pix[si+6]
+
+ di += 4
+ si += 8
+
+ }
+ }
+ })
+
+ case *image.RGBA:
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ di := dst.PixOffset(0, dstY)
+ si := src.PixOffset(srcMinX, srcMinY+dstY)
+ for dstX := 0; dstX < dstW; dstX++ {
+
+ a := src.Pix[si+3]
+ dst.Pix[di+3] = a
+ switch a {
+ case 0:
+ dst.Pix[di+0] = 0
+ dst.Pix[di+1] = 0
+ dst.Pix[di+2] = 0
+ case 0xff:
+ dst.Pix[di+0] = src.Pix[si+0]
+ dst.Pix[di+1] = src.Pix[si+1]
+ dst.Pix[di+2] = src.Pix[si+2]
+ default:
+ dst.Pix[di+0] = uint8(uint16(src.Pix[si+0]) * 0xff / uint16(a))
+ dst.Pix[di+1] = uint8(uint16(src.Pix[si+1]) * 0xff / uint16(a))
+ dst.Pix[di+2] = uint8(uint16(src.Pix[si+2]) * 0xff / uint16(a))
+ }
+
+ di += 4
+ si += 4
+
+ }
+ }
+ })
+
+ case *image.RGBA64:
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ di := dst.PixOffset(0, dstY)
+ si := src.PixOffset(srcMinX, srcMinY+dstY)
+ for dstX := 0; dstX < dstW; dstX++ {
+
+ a := src.Pix[si+6]
+ dst.Pix[di+3] = a
+ switch a {
+ case 0:
+ dst.Pix[di+0] = 0
+ dst.Pix[di+1] = 0
+ dst.Pix[di+2] = 0
+ case 0xff:
+ dst.Pix[di+0] = src.Pix[si+0]
+ dst.Pix[di+1] = src.Pix[si+2]
+ dst.Pix[di+2] = src.Pix[si+4]
+ default:
+ dst.Pix[di+0] = uint8(uint16(src.Pix[si+0]) * 0xff / uint16(a))
+ dst.Pix[di+1] = uint8(uint16(src.Pix[si+2]) * 0xff / uint16(a))
+ dst.Pix[di+2] = uint8(uint16(src.Pix[si+4]) * 0xff / uint16(a))
+ }
+
+ di += 4
+ si += 8
+
+ }
+ }
+ })
+
+ case *image.Gray:
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ di := dst.PixOffset(0, dstY)
+ si := src.PixOffset(srcMinX, srcMinY+dstY)
+ for dstX := 0; dstX < dstW; dstX++ {
+
+ c := src.Pix[si]
+ dst.Pix[di+0] = c
+ dst.Pix[di+1] = c
+ dst.Pix[di+2] = c
+ dst.Pix[di+3] = 0xff
+
+ di += 4
+ si += 1
+
+ }
+ }
+ })
+
+ case *image.Gray16:
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ di := dst.PixOffset(0, dstY)
+ si := src.PixOffset(srcMinX, srcMinY+dstY)
+ for dstX := 0; dstX < dstW; dstX++ {
+
+ c := src.Pix[si]
+ dst.Pix[di+0] = c
+ dst.Pix[di+1] = c
+ dst.Pix[di+2] = c
+ dst.Pix[di+3] = 0xff
+
+ di += 4
+ si += 2
+
+ }
+ }
+ })
+
+ case *image.YCbCr:
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ di := dst.PixOffset(0, dstY)
+ for dstX := 0; dstX < dstW; dstX++ {
+
+ srcX := srcMinX + dstX
+ srcY := srcMinY + dstY
+ siy := src.YOffset(srcX, srcY)
+ sic := src.COffset(srcX, srcY)
+ r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic])
+ dst.Pix[di+0] = r
+ dst.Pix[di+1] = g
+ dst.Pix[di+2] = b
+ dst.Pix[di+3] = 0xff
+
+ di += 4
+
+ }
+ }
+ })
+
+ case *image.Paletted:
+ plen := len(src.Palette)
+ pnew := make([]color.NRGBA, plen)
+ for i := 0; i < plen; i++ {
+ pnew[i] = color.NRGBAModel.Convert(src.Palette[i]).(color.NRGBA)
+ }
+
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ di := dst.PixOffset(0, dstY)
+ si := src.PixOffset(srcMinX, srcMinY+dstY)
+ for dstX := 0; dstX < dstW; dstX++ {
+
+ c := pnew[src.Pix[si]]
+ dst.Pix[di+0] = c.R
+ dst.Pix[di+1] = c.G
+ dst.Pix[di+2] = c.B
+ dst.Pix[di+3] = c.A
+
+ di += 4
+ si += 1
+
+ }
+ }
+ })
+
+ default:
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ di := dst.PixOffset(0, dstY)
+ for dstX := 0; dstX < dstW; dstX++ {
+
+ c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
+ dst.Pix[di+0] = c.R
+ dst.Pix[di+1] = c.G
+ dst.Pix[di+2] = c.B
+ dst.Pix[di+3] = c.A
+
+ di += 4
+
+ }
+ }
+ })
+
+ }
+
+ return dst
+}
+
+// This function used internally to convert any image type to NRGBA if needed.
+func toNRGBA(img image.Image) *image.NRGBA {
+ srcBounds := img.Bounds()
+ if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 {
+ if src0, ok := img.(*image.NRGBA); ok {
+ return src0
+ }
+ }
+ return Clone(img)
+}
diff --git a/Godeps/_workspace/src/github.com/disintegration/imaging/resize.go b/Godeps/_workspace/src/github.com/disintegration/imaging/resize.go
new file mode 100644
index 000000000..d2efd5c83
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/disintegration/imaging/resize.go
@@ -0,0 +1,564 @@
+package imaging
+
+import (
+ "image"
+ "math"
+)
+
+type iwpair struct {
+ i int
+ w int32
+}
+
+type pweights struct {
+ iwpairs []iwpair
+ wsum int32
+}
+
+func precomputeWeights(dstSize, srcSize int, filter ResampleFilter) []pweights {
+ du := float64(srcSize) / float64(dstSize)
+ scale := du
+ if scale < 1.0 {
+ scale = 1.0
+ }
+ ru := math.Ceil(scale * filter.Support)
+
+ out := make([]pweights, dstSize)
+
+ for v := 0; v < dstSize; v++ {
+ fu := (float64(v)+0.5)*du - 0.5
+
+ startu := int(math.Ceil(fu - ru))
+ if startu < 0 {
+ startu = 0
+ }
+ endu := int(math.Floor(fu + ru))
+ if endu > srcSize-1 {
+ endu = srcSize - 1
+ }
+
+ wsum := int32(0)
+ for u := startu; u <= endu; u++ {
+ w := int32(0xff * filter.Kernel((float64(u)-fu)/scale))
+ if w != 0 {
+ wsum += w
+ out[v].iwpairs = append(out[v].iwpairs, iwpair{u, w})
+ }
+ }
+ out[v].wsum = wsum
+ }
+
+ return out
+}
+
+// Resize resizes the image to the specified width and height using the specified resampling
+// filter and returns the transformed image. If one of width or height is 0, the image aspect
+// ratio is preserved.
+//
+// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
+// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
+//
+// Usage example:
+//
+// dstImage := imaging.Resize(srcImage, 800, 600, imaging.Lanczos)
+//
+func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
+ dstW, dstH := width, height
+
+ if dstW < 0 || dstH < 0 {
+ return &image.NRGBA{}
+ }
+ if dstW == 0 && dstH == 0 {
+ return &image.NRGBA{}
+ }
+
+ src := toNRGBA(img)
+
+ srcW := src.Bounds().Max.X
+ srcH := src.Bounds().Max.Y
+
+ if srcW <= 0 || srcH <= 0 {
+ return &image.NRGBA{}
+ }
+
+ // if new width or height is 0 then preserve aspect ratio, minimum 1px
+ if dstW == 0 {
+ tmpW := float64(dstH) * float64(srcW) / float64(srcH)
+ dstW = int(math.Max(1.0, math.Floor(tmpW+0.5)))
+ }
+ if dstH == 0 {
+ tmpH := float64(dstW) * float64(srcH) / float64(srcW)
+ dstH = int(math.Max(1.0, math.Floor(tmpH+0.5)))
+ }
+
+ var dst *image.NRGBA
+
+ if filter.Support <= 0.0 {
+ // nearest-neighbor special case
+ dst = resizeNearest(src, dstW, dstH)
+
+ } else {
+ // two-pass resize
+ if srcW != dstW {
+ dst = resizeHorizontal(src, dstW, filter)
+ } else {
+ dst = src
+ }
+
+ if srcH != dstH {
+ dst = resizeVertical(dst, dstH, filter)
+ }
+ }
+
+ return dst
+}
+
+func resizeHorizontal(src *image.NRGBA, width int, filter ResampleFilter) *image.NRGBA {
+ srcBounds := src.Bounds()
+ srcW := srcBounds.Max.X
+ srcH := srcBounds.Max.Y
+
+ dstW := width
+ dstH := srcH
+
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ weights := precomputeWeights(dstW, srcW, filter)
+
+ parallel(dstH, func(partStart, partEnd int) {
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ for dstX := 0; dstX < dstW; dstX++ {
+ var c [4]int32
+ for _, iw := range weights[dstX].iwpairs {
+ i := dstY*src.Stride + iw.i*4
+ c[0] += int32(src.Pix[i+0]) * iw.w
+ c[1] += int32(src.Pix[i+1]) * iw.w
+ c[2] += int32(src.Pix[i+2]) * iw.w
+ c[3] += int32(src.Pix[i+3]) * iw.w
+ }
+ j := dstY*dst.Stride + dstX*4
+ sum := weights[dstX].wsum
+ dst.Pix[j+0] = clampint32(int32(float32(c[0])/float32(sum) + 0.5))
+ dst.Pix[j+1] = clampint32(int32(float32(c[1])/float32(sum) + 0.5))
+ dst.Pix[j+2] = clampint32(int32(float32(c[2])/float32(sum) + 0.5))
+ dst.Pix[j+3] = clampint32(int32(float32(c[3])/float32(sum) + 0.5))
+ }
+ }
+ })
+
+ return dst
+}
+
+func resizeVertical(src *image.NRGBA, height int, filter ResampleFilter) *image.NRGBA {
+ srcBounds := src.Bounds()
+ srcW := srcBounds.Max.X
+ srcH := srcBounds.Max.Y
+
+ dstW := srcW
+ dstH := height
+
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ weights := precomputeWeights(dstH, srcH, filter)
+
+ parallel(dstW, func(partStart, partEnd int) {
+
+ for dstX := partStart; dstX < partEnd; dstX++ {
+ for dstY := 0; dstY < dstH; dstY++ {
+ var c [4]int32
+ for _, iw := range weights[dstY].iwpairs {
+ i := iw.i*src.Stride + dstX*4
+ c[0] += int32(src.Pix[i+0]) * iw.w
+ c[1] += int32(src.Pix[i+1]) * iw.w
+ c[2] += int32(src.Pix[i+2]) * iw.w
+ c[3] += int32(src.Pix[i+3]) * iw.w
+ }
+ j := dstY*dst.Stride + dstX*4
+ sum := weights[dstY].wsum
+ dst.Pix[j+0] = clampint32(int32(float32(c[0])/float32(sum) + 0.5))
+ dst.Pix[j+1] = clampint32(int32(float32(c[1])/float32(sum) + 0.5))
+ dst.Pix[j+2] = clampint32(int32(float32(c[2])/float32(sum) + 0.5))
+ dst.Pix[j+3] = clampint32(int32(float32(c[3])/float32(sum) + 0.5))
+ }
+ }
+
+ })
+
+ return dst
+}
+
+// fast nearest-neighbor resize, no filtering
+func resizeNearest(src *image.NRGBA, width, height int) *image.NRGBA {
+ dstW, dstH := width, height
+
+ srcBounds := src.Bounds()
+ srcW := srcBounds.Max.X
+ srcH := srcBounds.Max.Y
+
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ dx := float64(srcW) / float64(dstW)
+ dy := float64(srcH) / float64(dstH)
+
+ parallel(dstH, func(partStart, partEnd int) {
+
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ fy := (float64(dstY)+0.5)*dy - 0.5
+
+ for dstX := 0; dstX < dstW; dstX++ {
+ fx := (float64(dstX)+0.5)*dx - 0.5
+
+ srcX := int(math.Min(math.Max(math.Floor(fx+0.5), 0.0), float64(srcW)))
+ srcY := int(math.Min(math.Max(math.Floor(fy+0.5), 0.0), float64(srcH)))
+
+ srcOff := srcY*src.Stride + srcX*4
+ dstOff := dstY*dst.Stride + dstX*4
+
+ copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
+ }
+ }
+
+ })
+
+ return dst
+}
+
+// Fit scales down the image using the specified resample filter to fit the specified
+// maximum width and height and returns the transformed image.
+//
+// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
+// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
+//
+// Usage example:
+//
+// dstImage := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
+//
+func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
+ maxW, maxH := width, height
+
+ if maxW <= 0 || maxH <= 0 {
+ return &image.NRGBA{}
+ }
+
+ srcBounds := img.Bounds()
+ srcW := srcBounds.Dx()
+ srcH := srcBounds.Dy()
+
+ if srcW <= 0 || srcH <= 0 {
+ return &image.NRGBA{}
+ }
+
+ if srcW <= maxW && srcH <= maxH {
+ return Clone(img)
+ }
+
+ srcAspectRatio := float64(srcW) / float64(srcH)
+ maxAspectRatio := float64(maxW) / float64(maxH)
+
+ var newW, newH int
+ if srcAspectRatio > maxAspectRatio {
+ newW = maxW
+ newH = int(float64(newW) / srcAspectRatio)
+ } else {
+ newH = maxH
+ newW = int(float64(newH) * srcAspectRatio)
+ }
+
+ return Resize(img, newW, newH, filter)
+}
+
+// Thumbnail scales the image up or down using the specified resample filter, crops it
+// to the specified width and hight and returns the transformed image.
+//
+// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
+// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
+//
+// Usage example:
+//
+// dstImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
+//
+func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
+ thumbW, thumbH := width, height
+
+ if thumbW <= 0 || thumbH <= 0 {
+ return &image.NRGBA{}
+ }
+
+ srcBounds := img.Bounds()
+ srcW := srcBounds.Dx()
+ srcH := srcBounds.Dy()
+
+ if srcW <= 0 || srcH <= 0 {
+ return &image.NRGBA{}
+ }
+
+ srcAspectRatio := float64(srcW) / float64(srcH)
+ thumbAspectRatio := float64(thumbW) / float64(thumbH)
+
+ var tmp image.Image
+ if srcAspectRatio > thumbAspectRatio {
+ tmp = Resize(img, 0, thumbH, filter)
+ } else {
+ tmp = Resize(img, thumbW, 0, filter)
+ }
+
+ return CropCenter(tmp, thumbW, thumbH)
+}
+
+// Resample filter struct. It can be used to make custom filters.
+//
+// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
+// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
+//
+// General filter recommendations:
+//
+// - Lanczos
+// Probably the best resampling filter for photographic images yielding sharp results,
+// but it's slower than cubic filters (see below).
+//
+// - CatmullRom
+// A sharp cubic filter. It's a good filter for both upscaling and downscaling if sharp results are needed.
+//
+// - MitchellNetravali
+// A high quality cubic filter that produces smoother results with less ringing than CatmullRom.
+//
+// - BSpline
+// A good filter if a very smooth output is needed.
+//
+// - Linear
+// Bilinear interpolation filter, produces reasonably good, smooth output. It's faster than cubic filters.
+//
+// - Box
+// Simple and fast resampling filter appropriate for downscaling.
+// When upscaling it's similar to NearestNeighbor.
+//
+// - NearestNeighbor
+// Fastest resample filter, no antialiasing at all. Rarely used.
+//
+type ResampleFilter struct {
+ Support float64
+ Kernel func(float64) float64
+}
+
+// Nearest-neighbor filter, no anti-aliasing.
+var NearestNeighbor ResampleFilter
+
+// Box filter (averaging pixels).
+var Box ResampleFilter
+
+// Linear filter.
+var Linear ResampleFilter
+
+// Hermite cubic spline filter (BC-spline; B=0; C=0).
+var Hermite ResampleFilter
+
+// Mitchell-Netravali cubic filter (BC-spline; B=1/3; C=1/3).
+var MitchellNetravali ResampleFilter
+
+// Catmull-Rom - sharp cubic filter (BC-spline; B=0; C=0.5).
+var CatmullRom ResampleFilter
+
+// Cubic B-spline - smooth cubic filter (BC-spline; B=1; C=0).
+var BSpline ResampleFilter
+
+// Gaussian Blurring Filter.
+var Gaussian ResampleFilter
+
+// Bartlett-windowed sinc filter (3 lobes).
+var Bartlett ResampleFilter
+
+// Lanczos filter (3 lobes).
+var Lanczos ResampleFilter
+
+// Hann-windowed sinc filter (3 lobes).
+var Hann ResampleFilter
+
+// Hamming-windowed sinc filter (3 lobes).
+var Hamming ResampleFilter
+
+// Blackman-windowed sinc filter (3 lobes).
+var Blackman ResampleFilter
+
+// Welch-windowed sinc filter (parabolic window, 3 lobes).
+var Welch ResampleFilter
+
+// Cosine-windowed sinc filter (3 lobes).
+var Cosine ResampleFilter
+
+func bcspline(x, b, c float64) float64 {
+ x = math.Abs(x)
+ if x < 1.0 {
+ return ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6
+ }
+ if x < 2.0 {
+ return ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6
+ }
+ return 0
+}
+
+func sinc(x float64) float64 {
+ if x == 0 {
+ return 1
+ }
+ return math.Sin(math.Pi*x) / (math.Pi * x)
+}
+
+func init() {
+ NearestNeighbor = ResampleFilter{
+ Support: 0.0, // special case - not applying the filter
+ }
+
+ Box = ResampleFilter{
+ Support: 0.5,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x <= 0.5 {
+ return 1.0
+ }
+ return 0
+ },
+ }
+
+ Linear = ResampleFilter{
+ Support: 1.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 1.0 {
+ return 1.0 - x
+ }
+ return 0
+ },
+ }
+
+ Hermite = ResampleFilter{
+ Support: 1.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 1.0 {
+ return bcspline(x, 0.0, 0.0)
+ }
+ return 0
+ },
+ }
+
+ MitchellNetravali = ResampleFilter{
+ Support: 2.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 2.0 {
+ return bcspline(x, 1.0/3.0, 1.0/3.0)
+ }
+ return 0
+ },
+ }
+
+ CatmullRom = ResampleFilter{
+ Support: 2.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 2.0 {
+ return bcspline(x, 0.0, 0.5)
+ }
+ return 0
+ },
+ }
+
+ BSpline = ResampleFilter{
+ Support: 2.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 2.0 {
+ return bcspline(x, 1.0, 0.0)
+ }
+ return 0
+ },
+ }
+
+ Gaussian = ResampleFilter{
+ Support: 2.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 2.0 {
+ return math.Exp(-2 * x * x)
+ }
+ return 0
+ },
+ }
+
+ Bartlett = ResampleFilter{
+ Support: 3.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 3.0 {
+ return sinc(x) * (3.0 - x) / 3.0
+ }
+ return 0
+ },
+ }
+
+ Lanczos = ResampleFilter{
+ Support: 3.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 3.0 {
+ return sinc(x) * sinc(x/3.0)
+ }
+ return 0
+ },
+ }
+
+ Hann = ResampleFilter{
+ Support: 3.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 3.0 {
+ return sinc(x) * (0.5 + 0.5*math.Cos(math.Pi*x/3.0))
+ }
+ return 0
+ },
+ }
+
+ Hamming = ResampleFilter{
+ Support: 3.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 3.0 {
+ return sinc(x) * (0.54 + 0.46*math.Cos(math.Pi*x/3.0))
+ }
+ return 0
+ },
+ }
+
+ Blackman = ResampleFilter{
+ Support: 3.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 3.0 {
+ return sinc(x) * (0.42 - 0.5*math.Cos(math.Pi*x/3.0+math.Pi) + 0.08*math.Cos(2.0*math.Pi*x/3.0))
+ }
+ return 0
+ },
+ }
+
+ Welch = ResampleFilter{
+ Support: 3.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 3.0 {
+ return sinc(x) * (1.0 - (x * x / 9.0))
+ }
+ return 0
+ },
+ }
+
+ Cosine = ResampleFilter{
+ Support: 3.0,
+ Kernel: func(x float64) float64 {
+ x = math.Abs(x)
+ if x < 3.0 {
+ return sinc(x) * math.Cos((math.Pi/2.0)*(x/3.0))
+ }
+ return 0
+ },
+ }
+}
diff --git a/Godeps/_workspace/src/github.com/disintegration/imaging/tools.go b/Godeps/_workspace/src/github.com/disintegration/imaging/tools.go
new file mode 100644
index 000000000..2c39c900a
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/disintegration/imaging/tools.go
@@ -0,0 +1,182 @@
+package imaging
+
+import (
+ "image"
+ "math"
+)
+
+// Anchor is the anchor point for image alignment.
+type Anchor int
+
+const (
+ Center Anchor = iota
+ TopLeft
+ Top
+ TopRight
+ Left
+ Right
+ BottomLeft
+ Bottom
+ BottomRight
+)
+
+func anchorPt(b image.Rectangle, w, h int, anchor Anchor) image.Point {
+ var x, y int
+ switch anchor {
+ case TopLeft:
+ x = b.Min.X
+ y = b.Min.Y
+ case Top:
+ x = b.Min.X + (b.Dx()-w)/2
+ y = b.Min.Y
+ case TopRight:
+ x = b.Max.X - w
+ y = b.Min.Y
+ case Left:
+ x = b.Min.X
+ y = b.Min.Y + (b.Dy()-h)/2
+ case Right:
+ x = b.Max.X - w
+ y = b.Min.Y + (b.Dy()-h)/2
+ case BottomLeft:
+ x = b.Min.X
+ y = b.Max.Y - h
+ case Bottom:
+ x = b.Min.X + (b.Dx()-w)/2
+ y = b.Max.Y - h
+ case BottomRight:
+ x = b.Max.X - w
+ y = b.Max.Y - h
+ default:
+ x = b.Min.X + (b.Dx()-w)/2
+ y = b.Min.Y + (b.Dy()-h)/2
+ }
+ return image.Pt(x, y)
+}
+
+// Crop cuts out a rectangular region with the specified bounds
+// from the image and returns the cropped image.
+func Crop(img image.Image, rect image.Rectangle) *image.NRGBA {
+ src := toNRGBA(img)
+ srcRect := rect.Sub(img.Bounds().Min)
+ sub := src.SubImage(srcRect)
+ return Clone(sub) // New image Bounds().Min point will be (0, 0)
+}
+
+// CropAnchor cuts out a rectangular region with the specified size
+// from the image using the specified anchor point and returns the cropped image.
+func CropAnchor(img image.Image, width, height int, anchor Anchor) *image.NRGBA {
+ srcBounds := img.Bounds()
+ pt := anchorPt(srcBounds, width, height, anchor)
+ r := image.Rect(0, 0, width, height).Add(pt)
+ b := srcBounds.Intersect(r)
+ return Crop(img, b)
+}
+
+// CropCenter cuts out a rectangular region with the specified size
+// from the center of the image and returns the cropped image.
+func CropCenter(img image.Image, width, height int) *image.NRGBA {
+ return CropAnchor(img, width, height, Center)
+}
+
+// Paste pastes the img image to the background image at the specified position and returns the combined image.
+func Paste(background, img image.Image, pos image.Point) *image.NRGBA {
+ src := toNRGBA(img)
+ dst := Clone(background) // cloned image bounds start at (0, 0)
+ startPt := pos.Sub(background.Bounds().Min) // so we should translate start point
+ endPt := startPt.Add(src.Bounds().Size())
+ pasteBounds := image.Rectangle{startPt, endPt}
+
+ if dst.Bounds().Overlaps(pasteBounds) {
+ intersectBounds := dst.Bounds().Intersect(pasteBounds)
+
+ rowSize := intersectBounds.Dx() * 4
+ numRows := intersectBounds.Dy()
+
+ srcStartX := intersectBounds.Min.X - pasteBounds.Min.X
+ srcStartY := intersectBounds.Min.Y - pasteBounds.Min.Y
+
+ i0 := dst.PixOffset(intersectBounds.Min.X, intersectBounds.Min.Y)
+ j0 := src.PixOffset(srcStartX, srcStartY)
+
+ di := dst.Stride
+ dj := src.Stride
+
+ for row := 0; row < numRows; row++ {
+ copy(dst.Pix[i0:i0+rowSize], src.Pix[j0:j0+rowSize])
+ i0 += di
+ j0 += dj
+ }
+ }
+
+ return dst
+}
+
+// PasteCenter pastes the img image to the center of the background image and returns the combined image.
+func PasteCenter(background, img image.Image) *image.NRGBA {
+ bgBounds := background.Bounds()
+ bgW := bgBounds.Dx()
+ bgH := bgBounds.Dy()
+ bgMinX := bgBounds.Min.X
+ bgMinY := bgBounds.Min.Y
+
+ centerX := bgMinX + bgW/2
+ centerY := bgMinY + bgH/2
+
+ x0 := centerX - img.Bounds().Dx()/2
+ y0 := centerY - img.Bounds().Dy()/2
+
+ return Paste(background, img, image.Pt(x0, y0))
+}
+
+// Overlay draws the img image over the background image at given position
+// and returns the combined image. Opacity parameter is the opacity of the img
+// image layer, used to compose the images, it must be from 0.0 to 1.0.
+//
+// Usage examples:
+//
+// // draw the sprite over the background at position (50, 50)
+// dstImage := imaging.Overlay(backgroundImage, spriteImage, image.Pt(50, 50), 1.0)
+//
+// // blend two opaque images of the same size
+// dstImage := imaging.Overlay(imageOne, imageTwo, image.Pt(0, 0), 0.5)
+//
+func Overlay(background, img image.Image, pos image.Point, opacity float64) *image.NRGBA {
+ opacity = math.Min(math.Max(opacity, 0.0), 1.0) // check: 0.0 <= opacity <= 1.0
+
+ src := toNRGBA(img)
+ dst := Clone(background) // cloned image bounds start at (0, 0)
+ startPt := pos.Sub(background.Bounds().Min) // so we should translate start point
+ endPt := startPt.Add(src.Bounds().Size())
+ pasteBounds := image.Rectangle{startPt, endPt}
+
+ if dst.Bounds().Overlaps(pasteBounds) {
+ intersectBounds := dst.Bounds().Intersect(pasteBounds)
+
+ for y := intersectBounds.Min.Y; y < intersectBounds.Max.Y; y++ {
+ for x := intersectBounds.Min.X; x < intersectBounds.Max.X; x++ {
+ i := y*dst.Stride + x*4
+
+ srcX := x - pasteBounds.Min.X
+ srcY := y - pasteBounds.Min.Y
+ j := srcY*src.Stride + srcX*4
+
+ a1 := float64(dst.Pix[i+3])
+ a2 := float64(src.Pix[j+3])
+
+ coef2 := opacity * a2 / 255.0
+ coef1 := (1 - coef2) * a1 / 255.0
+ coefSum := coef1 + coef2
+ coef1 /= coefSum
+ coef2 /= coefSum
+
+ dst.Pix[i+0] = uint8(float64(dst.Pix[i+0])*coef1 + float64(src.Pix[j+0])*coef2)
+ dst.Pix[i+1] = uint8(float64(dst.Pix[i+1])*coef1 + float64(src.Pix[j+1])*coef2)
+ dst.Pix[i+2] = uint8(float64(dst.Pix[i+2])*coef1 + float64(src.Pix[j+2])*coef2)
+ dst.Pix[i+3] = uint8(math.Min(a1+a2*opacity*(255.0-a1)/255.0, 255.0))
+ }
+ }
+ }
+
+ return dst
+}
diff --git a/Godeps/_workspace/src/github.com/disintegration/imaging/transform.go b/Godeps/_workspace/src/github.com/disintegration/imaging/transform.go
new file mode 100644
index 000000000..a11601bba
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/disintegration/imaging/transform.go
@@ -0,0 +1,201 @@
+package imaging
+
+import (
+ "image"
+)
+
+// Rotate90 rotates the image 90 degrees counterclockwise and returns the transformed image.
+func Rotate90(img image.Image) *image.NRGBA {
+ src := toNRGBA(img)
+ srcW := src.Bounds().Max.X
+ srcH := src.Bounds().Max.Y
+ dstW := srcH
+ dstH := srcW
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ parallel(dstH, func(partStart, partEnd int) {
+
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ for dstX := 0; dstX < dstW; dstX++ {
+ srcX := dstH - dstY - 1
+ srcY := dstX
+
+ srcOff := srcY*src.Stride + srcX*4
+ dstOff := dstY*dst.Stride + dstX*4
+
+ copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
+ }
+ }
+
+ })
+
+ return dst
+}
+
+// Rotate180 rotates the image 180 degrees counterclockwise and returns the transformed image.
+func Rotate180(img image.Image) *image.NRGBA {
+ src := toNRGBA(img)
+ srcW := src.Bounds().Max.X
+ srcH := src.Bounds().Max.Y
+ dstW := srcW
+ dstH := srcH
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ parallel(dstH, func(partStart, partEnd int) {
+
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ for dstX := 0; dstX < dstW; dstX++ {
+ srcX := dstW - dstX - 1
+ srcY := dstH - dstY - 1
+
+ srcOff := srcY*src.Stride + srcX*4
+ dstOff := dstY*dst.Stride + dstX*4
+
+ copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
+ }
+ }
+
+ })
+
+ return dst
+}
+
+// Rotate270 rotates the image 270 degrees counterclockwise and returns the transformed image.
+func Rotate270(img image.Image) *image.NRGBA {
+ src := toNRGBA(img)
+ srcW := src.Bounds().Max.X
+ srcH := src.Bounds().Max.Y
+ dstW := srcH
+ dstH := srcW
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ parallel(dstH, func(partStart, partEnd int) {
+
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ for dstX := 0; dstX < dstW; dstX++ {
+ srcX := dstY
+ srcY := dstW - dstX - 1
+
+ srcOff := srcY*src.Stride + srcX*4
+ dstOff := dstY*dst.Stride + dstX*4
+
+ copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
+ }
+ }
+
+ })
+
+ return dst
+}
+
+// FlipH flips the image horizontally (from left to right) and returns the transformed image.
+func FlipH(img image.Image) *image.NRGBA {
+ src := toNRGBA(img)
+ srcW := src.Bounds().Max.X
+ srcH := src.Bounds().Max.Y
+ dstW := srcW
+ dstH := srcH
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ parallel(dstH, func(partStart, partEnd int) {
+
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ for dstX := 0; dstX < dstW; dstX++ {
+ srcX := dstW - dstX - 1
+ srcY := dstY
+
+ srcOff := srcY*src.Stride + srcX*4
+ dstOff := dstY*dst.Stride + dstX*4
+
+ copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
+ }
+ }
+
+ })
+
+ return dst
+}
+
+// FlipV flips the image vertically (from top to bottom) and returns the transformed image.
+func FlipV(img image.Image) *image.NRGBA {
+ src := toNRGBA(img)
+ srcW := src.Bounds().Max.X
+ srcH := src.Bounds().Max.Y
+ dstW := srcW
+ dstH := srcH
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ parallel(dstH, func(partStart, partEnd int) {
+
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ for dstX := 0; dstX < dstW; dstX++ {
+ srcX := dstX
+ srcY := dstH - dstY - 1
+
+ srcOff := srcY*src.Stride + srcX*4
+ dstOff := dstY*dst.Stride + dstX*4
+
+ copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
+ }
+ }
+
+ })
+
+ return dst
+}
+
+// Transpose flips the image horizontally and rotates 90 degrees counter-clockwise.
+func Transpose(img image.Image) *image.NRGBA {
+ src := toNRGBA(img)
+ srcW := src.Bounds().Max.X
+ srcH := src.Bounds().Max.Y
+ dstW := srcH
+ dstH := srcW
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ parallel(dstH, func(partStart, partEnd int) {
+
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ for dstX := 0; dstX < dstW; dstX++ {
+ srcX := dstY
+ srcY := dstX
+
+ srcOff := srcY*src.Stride + srcX*4
+ dstOff := dstY*dst.Stride + dstX*4
+
+ copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
+ }
+ }
+
+ })
+
+ return dst
+}
+
+// Transverse flips the image vertically and rotates 90 degrees counter-clockwise.
+func Transverse(img image.Image) *image.NRGBA {
+ src := toNRGBA(img)
+ srcW := src.Bounds().Max.X
+ srcH := src.Bounds().Max.Y
+ dstW := srcH
+ dstH := srcW
+ dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+ parallel(dstH, func(partStart, partEnd int) {
+
+ for dstY := partStart; dstY < partEnd; dstY++ {
+ for dstX := 0; dstX < dstW; dstX++ {
+ srcX := dstH - dstY - 1
+ srcY := dstW - dstX - 1
+
+ srcOff := srcY*src.Stride + srcX*4
+ dstOff := dstY*dst.Stride + dstX*4
+
+ copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
+ }
+ }
+
+ })
+
+ return dst
+}
diff --git a/Godeps/_workspace/src/github.com/disintegration/imaging/utils.go b/Godeps/_workspace/src/github.com/disintegration/imaging/utils.go
new file mode 100644
index 000000000..8b1ab8adb
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/disintegration/imaging/utils.go
@@ -0,0 +1,77 @@
+package imaging
+
+import (
+ "math"
+ "runtime"
+ "sync"
+ "sync/atomic"
+)
+
+var parallelizationEnabled = true
+
+// if GOMAXPROCS = 1: no goroutines used
+// if GOMAXPROCS > 1: spawn N=GOMAXPROCS workers in separate goroutines
+func parallel(dataSize int, fn func(partStart, partEnd int)) {
+ numGoroutines := 1
+ partSize := dataSize
+
+ if parallelizationEnabled {
+ numProcs := runtime.GOMAXPROCS(0)
+ if numProcs > 1 {
+ numGoroutines = numProcs
+ partSize = dataSize / (numGoroutines * 10)
+ if partSize < 1 {
+ partSize = 1
+ }
+ }
+ }
+
+ if numGoroutines == 1 {
+ fn(0, dataSize)
+ } else {
+ var wg sync.WaitGroup
+ wg.Add(numGoroutines)
+ idx := uint64(0)
+
+ for p := 0; p < numGoroutines; p++ {
+ go func() {
+ defer wg.Done()
+ for {
+ partStart := int(atomic.AddUint64(&idx, uint64(partSize))) - partSize
+ if partStart >= dataSize {
+ break
+ }
+ partEnd := partStart + partSize
+ if partEnd > dataSize {
+ partEnd = dataSize
+ }
+ fn(partStart, partEnd)
+ }
+ }()
+ }
+
+ wg.Wait()
+ }
+}
+
+func absint(i int) int {
+ if i < 0 {
+ return -i
+ }
+ return i
+}
+
+// clamp & round float64 to uint8 (0..255)
+func clamp(v float64) uint8 {
+ return uint8(math.Min(math.Max(v, 0.0), 255.0) + 0.5)
+}
+
+// clamp int32 to uint8 (0..255)
+func clampint32(v int32) uint8 {
+ if v < 0 {
+ return 0
+ } else if v > 255 {
+ return 255
+ }
+ return uint8(v)
+}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/.travis.yml b/Godeps/_workspace/src/github.com/nfnt/resize/.travis.yml
deleted file mode 100644
index 57bd4a76e..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/.travis.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-language: go
-
-go:
- - 1.1
- - 1.2
- - 1.3
- - tip
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/LICENSE b/Godeps/_workspace/src/github.com/nfnt/resize/LICENSE
deleted file mode 100644
index 7836cad5f..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/LICENSE
+++ /dev/null
@@ -1,13 +0,0 @@
-Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright notice
-and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-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/Godeps/_workspace/src/github.com/nfnt/resize/README.md b/Godeps/_workspace/src/github.com/nfnt/resize/README.md
deleted file mode 100644
index 2aefa75c9..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/README.md
+++ /dev/null
@@ -1,149 +0,0 @@
-Resize
-======
-
-Image resizing for the [Go programming language](http://golang.org) with common interpolation methods.
-
-[![Build Status](https://travis-ci.org/nfnt/resize.svg)](https://travis-ci.org/nfnt/resize)
-
-Installation
-------------
-
-```bash
-$ go get github.com/nfnt/resize
-```
-
-It's that easy!
-
-Usage
------
-
-This package needs at least Go 1.1. Import package with
-
-```go
-import "github.com/nfnt/resize"
-```
-
-The resize package provides 2 functions:
-
-* `resize.Resize` creates a scaled image with new dimensions (`width`, `height`) using the interpolation function `interp`.
- If either `width` or `height` is set to 0, it will be set to an aspect ratio preserving value.
-* `resize.Thumbnail` downscales an image preserving its aspect ratio to the maximum dimensions (`maxWidth`, `maxHeight`).
- It will return the original image if original sizes are smaller than the provided dimensions.
-
-```go
-resize.Resize(width, height uint, img image.Image, interp resize.InterpolationFunction) image.Image
-resize.Thumbnail(maxWidth, maxHeight uint, img image.Image, interp resize.InterpolationFunction) image.Image
-```
-
-The provided interpolation functions are (from fast to slow execution time)
-
-- `NearestNeighbor`: [Nearest-neighbor interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation)
-- `Bilinear`: [Bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation)
-- `Bicubic`: [Bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation)
-- `MitchellNetravali`: [Mitchell-Netravali interpolation](http://dl.acm.org/citation.cfm?id=378514)
-- `Lanczos2`: [Lanczos resampling](http://en.wikipedia.org/wiki/Lanczos_resampling) with a=2
-- `Lanczos3`: [Lanczos resampling](http://en.wikipedia.org/wiki/Lanczos_resampling) with a=3
-
-Which of these methods gives the best results depends on your use case.
-
-Sample usage:
-
-```go
-package main
-
-import (
- "github.com/nfnt/resize"
- "image/jpeg"
- "log"
- "os"
-)
-
-func main() {
- // open "test.jpg"
- file, err := os.Open("test.jpg")
- if err != nil {
- log.Fatal(err)
- }
-
- // decode jpeg into image.Image
- img, err := jpeg.Decode(file)
- if err != nil {
- log.Fatal(err)
- }
- file.Close()
-
- // resize to width 1000 using Lanczos resampling
- // and preserve aspect ratio
- m := resize.Resize(1000, 0, img, resize.Lanczos3)
-
- out, err := os.Create("test_resized.jpg")
- if err != nil {
- log.Fatal(err)
- }
- defer out.Close()
-
- // write new image to file
- jpeg.Encode(out, m, nil)
-}
-```
-
-Caveats
--------
-
-* Optimized access routines are used for `image.RGBA`, `image.NRGBA`, `image.RGBA64`, `image.NRGBA64`, `image.YCbCr`, `image.Gray`, and `image.Gray16` types. All other image types are accessed in a generic way that will result in slow processing speed.
-* JPEG images are stored in `image.YCbCr`. This image format stores data in a way that will decrease processing speed. A resize may be up to 2 times slower than with `image.RGBA`.
-
-
-Downsizing Samples
--------
-
-Downsizing is not as simple as it might look like. Images have to be filtered before they are scaled down, otherwise aliasing might occur.
-Filtering is highly subjective: Applying too much will blur the whole image, too little will make aliasing become apparent.
-Resize tries to provide sane defaults that should suffice in most cases.
-
-### Artificial sample
-
-Original image
-![Rings](http://nfnt.github.com/img/rings_lg_orig.png)
-
-<table>
-<tr>
-<th><img src="http://nfnt.github.com/img/rings_300_NearestNeighbor.png" /><br>Nearest-Neighbor</th>
-<th><img src="http://nfnt.github.com/img/rings_300_Bilinear.png" /><br>Bilinear</th>
-</tr>
-<tr>
-<th><img src="http://nfnt.github.com/img/rings_300_Bicubic.png" /><br>Bicubic</th>
-<th><img src="http://nfnt.github.com/img/rings_300_MitchellNetravali.png" /><br>Mitchell-Netravali</th>
-</tr>
-<tr>
-<th><img src="http://nfnt.github.com/img/rings_300_Lanczos2.png" /><br>Lanczos2</th>
-<th><img src="http://nfnt.github.com/img/rings_300_Lanczos3.png" /><br>Lanczos3</th>
-</tr>
-</table>
-
-### Real-Life sample
-
-Original image
-![Original](http://nfnt.github.com/img/IMG_3694_720.jpg)
-
-<table>
-<tr>
-<th><img src="http://nfnt.github.com/img/IMG_3694_300_NearestNeighbor.png" /><br>Nearest-Neighbor</th>
-<th><img src="http://nfnt.github.com/img/IMG_3694_300_Bilinear.png" /><br>Bilinear</th>
-</tr>
-<tr>
-<th><img src="http://nfnt.github.com/img/IMG_3694_300_Bicubic.png" /><br>Bicubic</th>
-<th><img src="http://nfnt.github.com/img/IMG_3694_300_MitchellNetravali.png" /><br>Mitchell-Netravali</th>
-</tr>
-<tr>
-<th><img src="http://nfnt.github.com/img/IMG_3694_300_Lanczos2.png" /><br>Lanczos2</th>
-<th><img src="http://nfnt.github.com/img/IMG_3694_300_Lanczos3.png" /><br>Lanczos3</th>
-</tr>
-</table>
-
-
-License
--------
-
-Copyright (c) 2012 Jan Schlicht <janschlicht@gmail.com>
-Resize is released under a MIT style license.
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/converter.go b/Godeps/_workspace/src/github.com/nfnt/resize/converter.go
deleted file mode 100644
index b3dd06b8d..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/converter.go
+++ /dev/null
@@ -1,452 +0,0 @@
-/*
-Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright notice
-and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-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.
-*/
-
-package resize
-
-import "image"
-
-// Keep value in [0,255] range.
-func clampUint8(in int32) uint8 {
- // casting a negative int to an uint will result in an overflown
- // large uint. this behavior will be exploited here and in other functions
- // to achieve a higher performance.
- if uint32(in) < 256 {
- return uint8(in)
- }
- if in > 255 {
- return 255
- }
- return 0
-}
-
-// Keep value in [0,65535] range.
-func clampUint16(in int64) uint16 {
- if uint64(in) < 65536 {
- return uint16(in)
- }
- if in > 65535 {
- return 65535
- }
- return 0
-}
-
-func resizeGeneric(in image.Image, out *image.NRGBA64, scale float64, coeffs []int32, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]int64
- var sum int64
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- coeff := coeffs[ci+i]
- if coeff != 0 {
- xi := start + i
- switch {
- case xi < 0:
- xi = 0
- case xi >= maxX:
- xi = maxX
- }
- r, g, b, a := in.At(xi+in.Bounds().Min.X, x+in.Bounds().Min.Y).RGBA()
-
- // reverse alpha-premultiplication.
- if a != 0 {
- r *= 0xffff
- r /= a
- g *= 0xffff
- g /= a
- b *= 0xffff
- b /= a
- }
-
- rgba[0] += int64(coeff) * int64(r)
- rgba[1] += int64(coeff) * int64(g)
- rgba[2] += int64(coeff) * int64(b)
- rgba[3] += int64(coeff) * int64(a)
- sum += int64(coeff)
- }
- }
-
- offset := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8
- value := clampUint16(rgba[0] / sum)
- out.Pix[offset+0] = uint8(value >> 8)
- out.Pix[offset+1] = uint8(value)
- value = clampUint16(rgba[1] / sum)
- out.Pix[offset+2] = uint8(value >> 8)
- out.Pix[offset+3] = uint8(value)
- value = clampUint16(rgba[2] / sum)
- out.Pix[offset+4] = uint8(value >> 8)
- out.Pix[offset+5] = uint8(value)
- value = clampUint16(rgba[3] / sum)
- out.Pix[offset+6] = uint8(value >> 8)
- out.Pix[offset+7] = uint8(value)
- }
- }
-}
-
-func resizeRGBA(in *image.RGBA, out *image.NRGBA, scale float64, coeffs []int16, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]int32
- var sum int32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- coeff := coeffs[ci+i]
- if coeff != 0 {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 4
- case xi >= maxX:
- xi = 4 * maxX
- default:
- xi = 0
- }
-
- r := uint32(row[xi+0])
- g := uint32(row[xi+1])
- b := uint32(row[xi+2])
- a := uint32(row[xi+3])
-
- // reverse alpha-premultiplication.
- if a != 0 {
- r *= 0xff
- r /= a
- g *= 0xff
- g /= a
- b *= 0xff
- b /= a
- }
-
- rgba[0] += int32(coeff) * int32(r)
- rgba[1] += int32(coeff) * int32(g)
- rgba[2] += int32(coeff) * int32(b)
- rgba[3] += int32(coeff) * int32(a)
- sum += int32(coeff)
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*4
- out.Pix[xo+0] = clampUint8(rgba[0] / sum)
- out.Pix[xo+1] = clampUint8(rgba[1] / sum)
- out.Pix[xo+2] = clampUint8(rgba[2] / sum)
- out.Pix[xo+3] = clampUint8(rgba[3] / sum)
- }
- }
-}
-
-func resizeNRGBA(in *image.NRGBA, out *image.NRGBA, scale float64, coeffs []int16, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]int32
- var sum int32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- coeff := coeffs[ci+i]
- if coeff != 0 {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 4
- case xi >= maxX:
- xi = 4 * maxX
- default:
- xi = 0
- }
- rgba[0] += int32(coeff) * int32(row[xi+0])
- rgba[1] += int32(coeff) * int32(row[xi+1])
- rgba[2] += int32(coeff) * int32(row[xi+2])
- rgba[3] += int32(coeff) * int32(row[xi+3])
- sum += int32(coeff)
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*4
- out.Pix[xo+0] = clampUint8(rgba[0] / sum)
- out.Pix[xo+1] = clampUint8(rgba[1] / sum)
- out.Pix[xo+2] = clampUint8(rgba[2] / sum)
- out.Pix[xo+3] = clampUint8(rgba[3] / sum)
- }
- }
-}
-
-func resizeRGBA64(in *image.RGBA64, out *image.NRGBA64, scale float64, coeffs []int32, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]int64
- var sum int64
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- coeff := coeffs[ci+i]
- if coeff != 0 {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 8
- case xi >= maxX:
- xi = 8 * maxX
- default:
- xi = 0
- }
-
- r := uint32(uint16(row[xi+0])<<8 | uint16(row[xi+1]))
- g := uint32(uint16(row[xi+2])<<8 | uint16(row[xi+3]))
- b := uint32(uint16(row[xi+4])<<8 | uint16(row[xi+5]))
- a := uint32(uint16(row[xi+6])<<8 | uint16(row[xi+7]))
-
- // reverse alpha-premultiplication.
- if a != 0 {
- r *= 0xffff
- r /= a
- g *= 0xffff
- g /= a
- b *= 0xffff
- b /= a
- }
-
- rgba[0] += int64(coeff) * int64(r)
- rgba[1] += int64(coeff) * int64(g)
- rgba[2] += int64(coeff) * int64(b)
- rgba[3] += int64(coeff) * int64(a)
- sum += int64(coeff)
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8
- value := clampUint16(rgba[0] / sum)
- out.Pix[xo+0] = uint8(value >> 8)
- out.Pix[xo+1] = uint8(value)
- value = clampUint16(rgba[1] / sum)
- out.Pix[xo+2] = uint8(value >> 8)
- out.Pix[xo+3] = uint8(value)
- value = clampUint16(rgba[2] / sum)
- out.Pix[xo+4] = uint8(value >> 8)
- out.Pix[xo+5] = uint8(value)
- value = clampUint16(rgba[3] / sum)
- out.Pix[xo+6] = uint8(value >> 8)
- out.Pix[xo+7] = uint8(value)
- }
- }
-}
-
-func resizeNRGBA64(in *image.NRGBA64, out *image.NRGBA64, scale float64, coeffs []int32, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]int64
- var sum int64
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- coeff := coeffs[ci+i]
- if coeff != 0 {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 8
- case xi >= maxX:
- xi = 8 * maxX
- default:
- xi = 0
- }
- rgba[0] += int64(coeff) * int64(uint16(row[xi+0])<<8|uint16(row[xi+1]))
- rgba[1] += int64(coeff) * int64(uint16(row[xi+2])<<8|uint16(row[xi+3]))
- rgba[2] += int64(coeff) * int64(uint16(row[xi+4])<<8|uint16(row[xi+5]))
- rgba[3] += int64(coeff) * int64(uint16(row[xi+6])<<8|uint16(row[xi+7]))
- sum += int64(coeff)
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8
- value := clampUint16(rgba[0] / sum)
- out.Pix[xo+0] = uint8(value >> 8)
- out.Pix[xo+1] = uint8(value)
- value = clampUint16(rgba[1] / sum)
- out.Pix[xo+2] = uint8(value >> 8)
- out.Pix[xo+3] = uint8(value)
- value = clampUint16(rgba[2] / sum)
- out.Pix[xo+4] = uint8(value >> 8)
- out.Pix[xo+5] = uint8(value)
- value = clampUint16(rgba[3] / sum)
- out.Pix[xo+6] = uint8(value >> 8)
- out.Pix[xo+7] = uint8(value)
- }
- }
-}
-
-func resizeGray(in *image.Gray, out *image.Gray, scale float64, coeffs []int16, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[(x-newBounds.Min.X)*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var gray int32
- var sum int32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- coeff := coeffs[ci+i]
- if coeff != 0 {
- xi := start + i
- switch {
- case xi < 0:
- xi = 0
- case xi >= maxX:
- xi = maxX
- }
- gray += int32(coeff) * int32(row[xi])
- sum += int32(coeff)
- }
- }
-
- offset := (y-newBounds.Min.Y)*out.Stride + (x - newBounds.Min.X)
- out.Pix[offset] = clampUint8(gray / sum)
- }
- }
-}
-
-func resizeGray16(in *image.Gray16, out *image.Gray16, scale float64, coeffs []int32, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var gray int64
- var sum int64
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- coeff := coeffs[ci+i]
- if coeff != 0 {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 2
- case xi >= maxX:
- xi = 2 * maxX
- default:
- xi = 0
- }
- gray += int64(coeff) * int64(uint16(row[xi+0])<<8|uint16(row[xi+1]))
- sum += int64(coeff)
- }
- }
-
- offset := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*2
- value := clampUint16(gray / sum)
- out.Pix[offset+0] = uint8(value >> 8)
- out.Pix[offset+1] = uint8(value)
- }
- }
-}
-
-func resizeYCbCr(in *ycc, out *ycc, scale float64, coeffs []int16, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var p [3]int32
- var sum int32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- coeff := coeffs[ci+i]
- if coeff != 0 {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 3
- case xi >= maxX:
- xi = 3 * maxX
- default:
- xi = 0
- }
- p[0] += int32(coeff) * int32(row[xi+0])
- p[1] += int32(coeff) * int32(row[xi+1])
- p[2] += int32(coeff) * int32(row[xi+2])
- sum += int32(coeff)
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*3
- out.Pix[xo+0] = clampUint8(p[0] / sum)
- out.Pix[xo+1] = clampUint8(p[1] / sum)
- out.Pix[xo+2] = clampUint8(p[2] / sum)
- }
- }
-}
-
-func nearestYCbCr(in *ycc, out *ycc, scale float64, coeffs []bool, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var p [3]float32
- var sum float32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- if coeffs[ci+i] {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 3
- case xi >= maxX:
- xi = 3 * maxX
- default:
- xi = 0
- }
- p[0] += float32(row[xi+0])
- p[1] += float32(row[xi+1])
- p[2] += float32(row[xi+2])
- sum++
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*3
- out.Pix[xo+0] = floatToUint8(p[0] / sum)
- out.Pix[xo+1] = floatToUint8(p[1] / sum)
- out.Pix[xo+2] = floatToUint8(p[2] / sum)
- }
- }
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/converter_test.go b/Godeps/_workspace/src/github.com/nfnt/resize/converter_test.go
deleted file mode 100644
index 85639efc2..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/converter_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package resize
-
-import (
- "testing"
-)
-
-func Test_ClampUint8(t *testing.T) {
- var testData = []struct {
- in int32
- expected uint8
- }{
- {0, 0},
- {255, 255},
- {128, 128},
- {-2, 0},
- {256, 255},
- }
- for _, test := range testData {
- actual := clampUint8(test.in)
- if actual != test.expected {
- t.Fail()
- }
- }
-}
-
-func Test_ClampUint16(t *testing.T) {
- var testData = []struct {
- in int64
- expected uint16
- }{
- {0, 0},
- {65535, 65535},
- {128, 128},
- {-2, 0},
- {65536, 65535},
- }
- for _, test := range testData {
- actual := clampUint16(test.in)
- if actual != test.expected {
- t.Fail()
- }
- }
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/filters.go b/Godeps/_workspace/src/github.com/nfnt/resize/filters.go
deleted file mode 100644
index 4ce04e389..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/filters.go
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
-Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright notice
-and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-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.
-*/
-
-package resize
-
-import (
- "math"
-)
-
-func nearest(in float64) float64 {
- if in >= -0.5 && in < 0.5 {
- return 1
- }
- return 0
-}
-
-func linear(in float64) float64 {
- in = math.Abs(in)
- if in <= 1 {
- return 1 - in
- }
- return 0
-}
-
-func cubic(in float64) float64 {
- in = math.Abs(in)
- if in <= 1 {
- return in*in*(1.5*in-2.5) + 1.0
- }
- if in <= 2 {
- return in*(in*(2.5-0.5*in)-4.0) + 2.0
- }
- return 0
-}
-
-func mitchellnetravali(in float64) float64 {
- in = math.Abs(in)
- if in <= 1 {
- return (7.0*in*in*in - 12.0*in*in + 5.33333333333) * 0.16666666666
- }
- if in <= 2 {
- return (-2.33333333333*in*in*in + 12.0*in*in - 20.0*in + 10.6666666667) * 0.16666666666
- }
- return 0
-}
-
-func sinc(x float64) float64 {
- x = math.Abs(x) * math.Pi
- if x >= 1.220703e-4 {
- return math.Sin(x) / x
- }
- return 1
-}
-
-func lanczos2(in float64) float64 {
- if in > -2 && in < 2 {
- return sinc(in) * sinc(in*0.5)
- }
- return 0
-}
-
-func lanczos3(in float64) float64 {
- if in > -3 && in < 3 {
- return sinc(in) * sinc(in*0.3333333333333333)
- }
- return 0
-}
-
-// range [-256,256]
-func createWeights8(dy, filterLength int, blur, scale float64, kernel func(float64) float64) ([]int16, []int, int) {
- filterLength = filterLength * int(math.Max(math.Ceil(blur*scale), 1))
- filterFactor := math.Min(1./(blur*scale), 1)
-
- coeffs := make([]int16, dy*filterLength)
- start := make([]int, dy)
- for y := 0; y < dy; y++ {
- interpX := scale*(float64(y)+0.5) - 0.5
- start[y] = int(interpX) - filterLength/2 + 1
- interpX -= float64(start[y])
- for i := 0; i < filterLength; i++ {
- in := (interpX - float64(i)) * filterFactor
- coeffs[y*filterLength+i] = int16(kernel(in) * 256)
- }
- }
-
- return coeffs, start, filterLength
-}
-
-// range [-65536,65536]
-func createWeights16(dy, filterLength int, blur, scale float64, kernel func(float64) float64) ([]int32, []int, int) {
- filterLength = filterLength * int(math.Max(math.Ceil(blur*scale), 1))
- filterFactor := math.Min(1./(blur*scale), 1)
-
- coeffs := make([]int32, dy*filterLength)
- start := make([]int, dy)
- for y := 0; y < dy; y++ {
- interpX := scale*(float64(y)+0.5) - 0.5
- start[y] = int(interpX) - filterLength/2 + 1
- interpX -= float64(start[y])
- for i := 0; i < filterLength; i++ {
- in := (interpX - float64(i)) * filterFactor
- coeffs[y*filterLength+i] = int32(kernel(in) * 65536)
- }
- }
-
- return coeffs, start, filterLength
-}
-
-func createWeightsNearest(dy, filterLength int, blur, scale float64) ([]bool, []int, int) {
- filterLength = filterLength * int(math.Max(math.Ceil(blur*scale), 1))
- filterFactor := math.Min(1./(blur*scale), 1)
-
- coeffs := make([]bool, dy*filterLength)
- start := make([]int, dy)
- for y := 0; y < dy; y++ {
- interpX := scale*(float64(y)+0.5) - 0.5
- start[y] = int(interpX) - filterLength/2 + 1
- interpX -= float64(start[y])
- for i := 0; i < filterLength; i++ {
- in := (interpX - float64(i)) * filterFactor
- if in >= -0.5 && in < 0.5 {
- coeffs[y*filterLength+i] = true
- } else {
- coeffs[y*filterLength+i] = false
- }
- }
- }
-
- return coeffs, start, filterLength
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/nearest.go b/Godeps/_workspace/src/github.com/nfnt/resize/nearest.go
deleted file mode 100644
index 888039d85..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/nearest.go
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
-Copyright (c) 2014, Charlie Vieth <charlie.vieth@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright notice
-and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-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.
-*/
-
-package resize
-
-import "image"
-
-func floatToUint8(x float32) uint8 {
- // Nearest-neighbor values are always
- // positive no need to check lower-bound.
- if x > 0xfe {
- return 0xff
- }
- return uint8(x)
-}
-
-func floatToUint16(x float32) uint16 {
- if x > 0xfffe {
- return 0xffff
- }
- return uint16(x)
-}
-
-func nearestGeneric(in image.Image, out *image.RGBA64, scale float64, coeffs []bool, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]float32
- var sum float32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- if coeffs[ci+i] {
- xi := start + i
- switch {
- case xi < 0:
- xi = 0
- case xi >= maxX:
- xi = maxX
- }
- r, g, b, a := in.At(xi+in.Bounds().Min.X, x+in.Bounds().Min.Y).RGBA()
- rgba[0] += float32(r)
- rgba[1] += float32(g)
- rgba[2] += float32(b)
- rgba[3] += float32(a)
- sum++
- }
- }
-
- offset := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8
- value := floatToUint16(rgba[0] / sum)
- out.Pix[offset+0] = uint8(value >> 8)
- out.Pix[offset+1] = uint8(value)
- value = floatToUint16(rgba[1] / sum)
- out.Pix[offset+2] = uint8(value >> 8)
- out.Pix[offset+3] = uint8(value)
- value = floatToUint16(rgba[2] / sum)
- out.Pix[offset+4] = uint8(value >> 8)
- out.Pix[offset+5] = uint8(value)
- value = floatToUint16(rgba[3] / sum)
- out.Pix[offset+6] = uint8(value >> 8)
- out.Pix[offset+7] = uint8(value)
- }
- }
-}
-
-func nearestRGBA(in *image.RGBA, out *image.RGBA, scale float64, coeffs []bool, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]float32
- var sum float32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- if coeffs[ci+i] {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 4
- case xi >= maxX:
- xi = 4 * maxX
- default:
- xi = 0
- }
- rgba[0] += float32(row[xi+0])
- rgba[1] += float32(row[xi+1])
- rgba[2] += float32(row[xi+2])
- rgba[3] += float32(row[xi+3])
- sum++
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*4
- out.Pix[xo+0] = floatToUint8(rgba[0] / sum)
- out.Pix[xo+1] = floatToUint8(rgba[1] / sum)
- out.Pix[xo+2] = floatToUint8(rgba[2] / sum)
- out.Pix[xo+3] = floatToUint8(rgba[3] / sum)
- }
- }
-}
-
-func nearestNRGBA(in *image.NRGBA, out *image.NRGBA, scale float64, coeffs []bool, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]float32
- var sum float32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- if coeffs[ci+i] {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 4
- case xi >= maxX:
- xi = 4 * maxX
- default:
- xi = 0
- }
- rgba[0] += float32(row[xi+0])
- rgba[1] += float32(row[xi+1])
- rgba[2] += float32(row[xi+2])
- rgba[3] += float32(row[xi+3])
- sum++
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*4
- out.Pix[xo+0] = floatToUint8(rgba[0] / sum)
- out.Pix[xo+1] = floatToUint8(rgba[1] / sum)
- out.Pix[xo+2] = floatToUint8(rgba[2] / sum)
- out.Pix[xo+3] = floatToUint8(rgba[3] / sum)
- }
- }
-}
-
-func nearestRGBA64(in *image.RGBA64, out *image.RGBA64, scale float64, coeffs []bool, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]float32
- var sum float32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- if coeffs[ci+i] {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 8
- case xi >= maxX:
- xi = 8 * maxX
- default:
- xi = 0
- }
- rgba[0] += float32(uint16(row[xi+0])<<8 | uint16(row[xi+1]))
- rgba[1] += float32(uint16(row[xi+2])<<8 | uint16(row[xi+3]))
- rgba[2] += float32(uint16(row[xi+4])<<8 | uint16(row[xi+5]))
- rgba[3] += float32(uint16(row[xi+6])<<8 | uint16(row[xi+7]))
- sum++
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8
- value := floatToUint16(rgba[0] / sum)
- out.Pix[xo+0] = uint8(value >> 8)
- out.Pix[xo+1] = uint8(value)
- value = floatToUint16(rgba[1] / sum)
- out.Pix[xo+2] = uint8(value >> 8)
- out.Pix[xo+3] = uint8(value)
- value = floatToUint16(rgba[2] / sum)
- out.Pix[xo+4] = uint8(value >> 8)
- out.Pix[xo+5] = uint8(value)
- value = floatToUint16(rgba[3] / sum)
- out.Pix[xo+6] = uint8(value >> 8)
- out.Pix[xo+7] = uint8(value)
- }
- }
-}
-
-func nearestNRGBA64(in *image.NRGBA64, out *image.NRGBA64, scale float64, coeffs []bool, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var rgba [4]float32
- var sum float32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- if coeffs[ci+i] {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 8
- case xi >= maxX:
- xi = 8 * maxX
- default:
- xi = 0
- }
- rgba[0] += float32(uint16(row[xi+0])<<8 | uint16(row[xi+1]))
- rgba[1] += float32(uint16(row[xi+2])<<8 | uint16(row[xi+3]))
- rgba[2] += float32(uint16(row[xi+4])<<8 | uint16(row[xi+5]))
- rgba[3] += float32(uint16(row[xi+6])<<8 | uint16(row[xi+7]))
- sum++
- }
- }
-
- xo := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*8
- value := floatToUint16(rgba[0] / sum)
- out.Pix[xo+0] = uint8(value >> 8)
- out.Pix[xo+1] = uint8(value)
- value = floatToUint16(rgba[1] / sum)
- out.Pix[xo+2] = uint8(value >> 8)
- out.Pix[xo+3] = uint8(value)
- value = floatToUint16(rgba[2] / sum)
- out.Pix[xo+4] = uint8(value >> 8)
- out.Pix[xo+5] = uint8(value)
- value = floatToUint16(rgba[3] / sum)
- out.Pix[xo+6] = uint8(value >> 8)
- out.Pix[xo+7] = uint8(value)
- }
- }
-}
-
-func nearestGray(in *image.Gray, out *image.Gray, scale float64, coeffs []bool, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var gray float32
- var sum float32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- if coeffs[ci+i] {
- xi := start + i
- switch {
- case xi < 0:
- xi = 0
- case xi >= maxX:
- xi = maxX
- }
- gray += float32(row[xi])
- sum++
- }
- }
-
- offset := (y-newBounds.Min.Y)*out.Stride + (x - newBounds.Min.X)
- out.Pix[offset] = floatToUint8(gray / sum)
- }
- }
-}
-
-func nearestGray16(in *image.Gray16, out *image.Gray16, scale float64, coeffs []bool, offset []int, filterLength int) {
- newBounds := out.Bounds()
- maxX := in.Bounds().Dx() - 1
-
- for x := newBounds.Min.X; x < newBounds.Max.X; x++ {
- row := in.Pix[x*in.Stride:]
- for y := newBounds.Min.Y; y < newBounds.Max.Y; y++ {
- var gray float32
- var sum float32
- start := offset[y]
- ci := y * filterLength
- for i := 0; i < filterLength; i++ {
- if coeffs[ci+i] {
- xi := start + i
- switch {
- case uint(xi) < uint(maxX):
- xi *= 2
- case xi >= maxX:
- xi = 2 * maxX
- default:
- xi = 0
- }
- gray += float32(uint16(row[xi+0])<<8 | uint16(row[xi+1]))
- sum++
- }
- }
-
- offset := (y-newBounds.Min.Y)*out.Stride + (x-newBounds.Min.X)*2
- value := floatToUint16(gray / sum)
- out.Pix[offset+0] = uint8(value >> 8)
- out.Pix[offset+1] = uint8(value)
- }
- }
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/nearest_test.go b/Godeps/_workspace/src/github.com/nfnt/resize/nearest_test.go
deleted file mode 100644
index d4a76dda5..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/nearest_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
-Copyright (c) 2014, Charlie Vieth <charlie.vieth@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright notice
-and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-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.
-*/
-
-package resize
-
-import "testing"
-
-func Test_FloatToUint8(t *testing.T) {
- var testData = []struct {
- in float32
- expected uint8
- }{
- {0, 0},
- {255, 255},
- {128, 128},
- {1, 1},
- {256, 255},
- }
- for _, test := range testData {
- actual := floatToUint8(test.in)
- if actual != test.expected {
- t.Fail()
- }
- }
-}
-
-func Test_FloatToUint16(t *testing.T) {
- var testData = []struct {
- in float32
- expected uint16
- }{
- {0, 0},
- {65535, 65535},
- {128, 128},
- {1, 1},
- {65536, 65535},
- }
- for _, test := range testData {
- actual := floatToUint16(test.in)
- if actual != test.expected {
- t.Fail()
- }
- }
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/resize.go b/Godeps/_workspace/src/github.com/nfnt/resize/resize.go
deleted file mode 100644
index c1672432e..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/resize.go
+++ /dev/null
@@ -1,614 +0,0 @@
-/*
-Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright notice
-and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-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.
-*/
-
-// Package resize implements various image resizing methods.
-//
-// The package works with the Image interface described in the image package.
-// Various interpolation methods are provided and multiple processors may be
-// utilized in the computations.
-//
-// Example:
-// imgResized := resize.Resize(1000, 0, imgOld, resize.MitchellNetravali)
-package resize
-
-import (
- "image"
- "runtime"
- "sync"
-)
-
-// An InterpolationFunction provides the parameters that describe an
-// interpolation kernel. It returns the number of samples to take
-// and the kernel function to use for sampling.
-type InterpolationFunction int
-
-// InterpolationFunction constants
-const (
- // Nearest-neighbor interpolation
- NearestNeighbor InterpolationFunction = iota
- // Bilinear interpolation
- Bilinear
- // Bicubic interpolation (with cubic hermite spline)
- Bicubic
- // Mitchell-Netravali interpolation
- MitchellNetravali
- // Lanczos interpolation (a=2)
- Lanczos2
- // Lanczos interpolation (a=3)
- Lanczos3
-)
-
-// kernal, returns an InterpolationFunctions taps and kernel.
-func (i InterpolationFunction) kernel() (int, func(float64) float64) {
- switch i {
- case Bilinear:
- return 2, linear
- case Bicubic:
- return 4, cubic
- case MitchellNetravali:
- return 4, mitchellnetravali
- case Lanczos2:
- return 4, lanczos2
- case Lanczos3:
- return 6, lanczos3
- default:
- // Default to NearestNeighbor.
- return 2, nearest
- }
-}
-
-// values <1 will sharpen the image
-var blur = 1.0
-
-// Resize scales an image to new width and height using the interpolation function interp.
-// A new image with the given dimensions will be returned.
-// If one of the parameters width or height is set to 0, its size will be calculated so that
-// the aspect ratio is that of the originating image.
-// The resizing algorithm uses channels for parallel computation.
-func Resize(width, height uint, img image.Image, interp InterpolationFunction) image.Image {
- scaleX, scaleY := calcFactors(width, height, float64(img.Bounds().Dx()), float64(img.Bounds().Dy()))
- if width == 0 {
- width = uint(0.7 + float64(img.Bounds().Dx())/scaleX)
- }
- if height == 0 {
- height = uint(0.7 + float64(img.Bounds().Dy())/scaleY)
- }
-
- // Trivial case: return input image
- if int(width) == img.Bounds().Dx() && int(height) == img.Bounds().Dy() {
- return img
- }
-
- if interp == NearestNeighbor {
- return resizeNearest(width, height, scaleX, scaleY, img, interp)
- }
-
- taps, kernel := interp.kernel()
- cpus := runtime.GOMAXPROCS(0)
- wg := sync.WaitGroup{}
-
- // Generic access to image.Image is slow in tight loops.
- // The optimal access has to be determined from the concrete image type.
- switch input := img.(type) {
- case *image.RGBA:
- // 8-bit precision
- temp := image.NewNRGBA(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewNRGBA(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeights8(temp.Bounds().Dy(), taps, blur, scaleX, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.NRGBA)
- go func() {
- defer wg.Done()
- resizeRGBA(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeights8(result.Bounds().Dy(), taps, blur, scaleY, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.NRGBA)
- go func() {
- defer wg.Done()
- resizeNRGBA(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- case *image.NRGBA:
- // 8-bit precision
- temp := image.NewNRGBA(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewNRGBA(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeights8(temp.Bounds().Dy(), taps, blur, scaleX, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.NRGBA)
- go func() {
- defer wg.Done()
- resizeNRGBA(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeights8(result.Bounds().Dy(), taps, blur, scaleY, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.NRGBA)
- go func() {
- defer wg.Done()
- resizeNRGBA(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
-
- case *image.YCbCr:
- // 8-bit precision
- // accessing the YCbCr arrays in a tight loop is slow.
- // converting the image to ycc increases performance by 2x.
- temp := newYCC(image.Rect(0, 0, input.Bounds().Dy(), int(width)), input.SubsampleRatio)
- result := newYCC(image.Rect(0, 0, int(width), int(height)), image.YCbCrSubsampleRatio444)
-
- coeffs, offset, filterLength := createWeights8(temp.Bounds().Dy(), taps, blur, scaleX, kernel)
- in := imageYCbCrToYCC(input)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*ycc)
- go func() {
- defer wg.Done()
- resizeYCbCr(in, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- coeffs, offset, filterLength = createWeights8(result.Bounds().Dy(), taps, blur, scaleY, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*ycc)
- go func() {
- defer wg.Done()
- resizeYCbCr(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result.YCbCr()
- case *image.RGBA64:
- // 16-bit precision
- temp := image.NewNRGBA64(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewNRGBA64(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeights16(temp.Bounds().Dy(), taps, blur, scaleX, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.NRGBA64)
- go func() {
- defer wg.Done()
- resizeRGBA64(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeights16(result.Bounds().Dy(), taps, blur, scaleY, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.NRGBA64)
- go func() {
- defer wg.Done()
- resizeNRGBA64(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- case *image.NRGBA64:
- // 16-bit precision
- temp := image.NewNRGBA64(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewNRGBA64(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeights16(temp.Bounds().Dy(), taps, blur, scaleX, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.NRGBA64)
- go func() {
- defer wg.Done()
- resizeNRGBA64(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeights16(result.Bounds().Dy(), taps, blur, scaleY, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.NRGBA64)
- go func() {
- defer wg.Done()
- resizeNRGBA64(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- case *image.Gray:
- // 8-bit precision
- temp := image.NewGray(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewGray(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeights8(temp.Bounds().Dy(), taps, blur, scaleX, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.Gray)
- go func() {
- defer wg.Done()
- resizeGray(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeights8(result.Bounds().Dy(), taps, blur, scaleY, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.Gray)
- go func() {
- defer wg.Done()
- resizeGray(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- case *image.Gray16:
- // 16-bit precision
- temp := image.NewGray16(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewGray16(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeights16(temp.Bounds().Dy(), taps, blur, scaleX, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.Gray16)
- go func() {
- defer wg.Done()
- resizeGray16(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeights16(result.Bounds().Dy(), taps, blur, scaleY, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.Gray16)
- go func() {
- defer wg.Done()
- resizeGray16(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- default:
- // 16-bit precision
- temp := image.NewNRGBA64(image.Rect(0, 0, img.Bounds().Dy(), int(width)))
- result := image.NewNRGBA64(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeights16(temp.Bounds().Dy(), taps, blur, scaleX, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.NRGBA64)
- go func() {
- defer wg.Done()
- resizeGeneric(img, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeights16(result.Bounds().Dy(), taps, blur, scaleY, kernel)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.NRGBA64)
- go func() {
- defer wg.Done()
- resizeNRGBA64(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- }
-}
-
-func resizeNearest(width, height uint, scaleX, scaleY float64, img image.Image, interp InterpolationFunction) image.Image {
- taps, _ := interp.kernel()
- cpus := runtime.GOMAXPROCS(0)
- wg := sync.WaitGroup{}
-
- switch input := img.(type) {
- case *image.RGBA:
- // 8-bit precision
- temp := image.NewRGBA(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewRGBA(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.RGBA)
- go func() {
- defer wg.Done()
- nearestRGBA(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.RGBA)
- go func() {
- defer wg.Done()
- nearestRGBA(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- case *image.NRGBA:
- // 8-bit precision
- temp := image.NewNRGBA(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewNRGBA(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.NRGBA)
- go func() {
- defer wg.Done()
- nearestNRGBA(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.NRGBA)
- go func() {
- defer wg.Done()
- nearestNRGBA(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- case *image.YCbCr:
- // 8-bit precision
- // accessing the YCbCr arrays in a tight loop is slow.
- // converting the image to ycc increases performance by 2x.
- temp := newYCC(image.Rect(0, 0, input.Bounds().Dy(), int(width)), input.SubsampleRatio)
- result := newYCC(image.Rect(0, 0, int(width), int(height)), image.YCbCrSubsampleRatio444)
-
- coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX)
- in := imageYCbCrToYCC(input)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*ycc)
- go func() {
- defer wg.Done()
- nearestYCbCr(in, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*ycc)
- go func() {
- defer wg.Done()
- nearestYCbCr(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result.YCbCr()
- case *image.RGBA64:
- // 16-bit precision
- temp := image.NewRGBA64(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewRGBA64(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.RGBA64)
- go func() {
- defer wg.Done()
- nearestRGBA64(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.RGBA64)
- go func() {
- defer wg.Done()
- nearestRGBA64(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- case *image.NRGBA64:
- // 16-bit precision
- temp := image.NewNRGBA64(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewNRGBA64(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.NRGBA64)
- go func() {
- defer wg.Done()
- nearestNRGBA64(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.NRGBA64)
- go func() {
- defer wg.Done()
- nearestNRGBA64(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- case *image.Gray:
- // 8-bit precision
- temp := image.NewGray(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewGray(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.Gray)
- go func() {
- defer wg.Done()
- nearestGray(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.Gray)
- go func() {
- defer wg.Done()
- nearestGray(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- case *image.Gray16:
- // 16-bit precision
- temp := image.NewGray16(image.Rect(0, 0, input.Bounds().Dy(), int(width)))
- result := image.NewGray16(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.Gray16)
- go func() {
- defer wg.Done()
- nearestGray16(input, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.Gray16)
- go func() {
- defer wg.Done()
- nearestGray16(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- default:
- // 16-bit precision
- temp := image.NewRGBA64(image.Rect(0, 0, img.Bounds().Dy(), int(width)))
- result := image.NewRGBA64(image.Rect(0, 0, int(width), int(height)))
-
- // horizontal filter, results in transposed temporary image
- coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(temp, i, cpus).(*image.RGBA64)
- go func() {
- defer wg.Done()
- nearestGeneric(img, slice, scaleX, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
-
- // horizontal filter on transposed image, result is not transposed
- coeffs, offset, filterLength = createWeightsNearest(result.Bounds().Dy(), taps, blur, scaleY)
- wg.Add(cpus)
- for i := 0; i < cpus; i++ {
- slice := makeSlice(result, i, cpus).(*image.RGBA64)
- go func() {
- defer wg.Done()
- nearestRGBA64(temp, slice, scaleY, coeffs, offset, filterLength)
- }()
- }
- wg.Wait()
- return result
- }
-
-}
-
-// Calculates scaling factors using old and new image dimensions.
-func calcFactors(width, height uint, oldWidth, oldHeight float64) (scaleX, scaleY float64) {
- if width == 0 {
- if height == 0 {
- scaleX = 1.0
- scaleY = 1.0
- } else {
- scaleY = oldHeight / float64(height)
- scaleX = scaleY
- }
- } else {
- scaleX = oldWidth / float64(width)
- if height == 0 {
- scaleY = scaleX
- } else {
- scaleY = oldHeight / float64(height)
- }
- }
- return
-}
-
-type imageWithSubImage interface {
- image.Image
- SubImage(image.Rectangle) image.Image
-}
-
-func makeSlice(img imageWithSubImage, i, n int) image.Image {
- return img.SubImage(image.Rect(img.Bounds().Min.X, img.Bounds().Min.Y+i*img.Bounds().Dy()/n, img.Bounds().Max.X, img.Bounds().Min.Y+(i+1)*img.Bounds().Dy()/n))
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/resize_test.go b/Godeps/_workspace/src/github.com/nfnt/resize/resize_test.go
deleted file mode 100644
index 6f6911316..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/resize_test.go
+++ /dev/null
@@ -1,314 +0,0 @@
-package resize
-
-import (
- "image"
- "image/color"
- "runtime"
- "testing"
-)
-
-var img = image.NewGray16(image.Rect(0, 0, 3, 3))
-
-func init() {
- runtime.GOMAXPROCS(runtime.NumCPU())
- img.Set(1, 1, color.White)
-}
-
-func Test_Param1(t *testing.T) {
- m := Resize(0, 0, img, NearestNeighbor)
- if m.Bounds() != img.Bounds() {
- t.Fail()
- }
-}
-
-func Test_Param2(t *testing.T) {
- m := Resize(100, 0, img, NearestNeighbor)
- if m.Bounds() != image.Rect(0, 0, 100, 100) {
- t.Fail()
- }
-}
-
-func Test_ZeroImg(t *testing.T) {
- zeroImg := image.NewGray16(image.Rect(0, 0, 0, 0))
-
- m := Resize(0, 0, zeroImg, NearestNeighbor)
- if m.Bounds() != zeroImg.Bounds() {
- t.Fail()
- }
-}
-
-func Test_CorrectResize(t *testing.T) {
- zeroImg := image.NewGray16(image.Rect(0, 0, 256, 256))
-
- m := Resize(60, 0, zeroImg, NearestNeighbor)
- if m.Bounds() != image.Rect(0, 0, 60, 60) {
- t.Fail()
- }
-}
-
-func Test_SameColorWithRGBA(t *testing.T) {
- img := image.NewRGBA(image.Rect(0, 0, 20, 20))
- for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
- for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
- img.SetRGBA(x, y, color.RGBA{0x80, 0x80, 0x80, 0xFF})
- }
- }
- out := Resize(10, 10, img, Lanczos3)
- for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ {
- for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ {
- color := out.At(x, y).(color.NRGBA)
- if color.R != 0x80 || color.G != 0x80 || color.B != 0x80 || color.A != 0xFF {
- t.Errorf("%+v", color)
- }
- }
- }
-}
-
-func Test_SameColorWithNRGBA(t *testing.T) {
- img := image.NewNRGBA(image.Rect(0, 0, 20, 20))
- for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
- for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
- img.SetNRGBA(x, y, color.NRGBA{0x80, 0x80, 0x80, 0xFF})
- }
- }
- out := Resize(10, 10, img, Lanczos3)
- for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ {
- for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ {
- color := out.At(x, y).(color.NRGBA)
- if color.R != 0x80 || color.G != 0x80 || color.B != 0x80 || color.A != 0xFF {
- t.Errorf("%+v", color)
- }
- }
- }
-}
-
-func Test_SameColorWithRGBA64(t *testing.T) {
- img := image.NewRGBA64(image.Rect(0, 0, 20, 20))
- for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
- for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
- img.SetRGBA64(x, y, color.RGBA64{0x8000, 0x8000, 0x8000, 0xFFFF})
- }
- }
- out := Resize(10, 10, img, Lanczos3)
- for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ {
- for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ {
- color := out.At(x, y).(color.NRGBA64)
- if color.R != 0x8000 || color.G != 0x8000 || color.B != 0x8000 || color.A != 0xFFFF {
- t.Errorf("%+v", color)
- }
- }
- }
-}
-
-func Test_SameColorWithNRGBA64(t *testing.T) {
- img := image.NewNRGBA64(image.Rect(0, 0, 20, 20))
- for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
- for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
- img.SetNRGBA64(x, y, color.NRGBA64{0x8000, 0x8000, 0x8000, 0xFFFF})
- }
- }
- out := Resize(10, 10, img, Lanczos3)
- for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ {
- for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ {
- color := out.At(x, y).(color.NRGBA64)
- if color.R != 0x8000 || color.G != 0x8000 || color.B != 0x8000 || color.A != 0xFFFF {
- t.Errorf("%+v", color)
- }
- }
- }
-}
-
-func Test_SameColorWithGray(t *testing.T) {
- img := image.NewGray(image.Rect(0, 0, 20, 20))
- for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
- for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
- img.SetGray(x, y, color.Gray{0x80})
- }
- }
- out := Resize(10, 10, img, Lanczos3)
- for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ {
- for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ {
- color := out.At(x, y).(color.Gray)
- if color.Y != 0x80 {
- t.Errorf("%+v", color)
- }
- }
- }
-}
-
-func Test_SameColorWithGray16(t *testing.T) {
- img := image.NewGray16(image.Rect(0, 0, 20, 20))
- for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
- for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
- img.SetGray16(x, y, color.Gray16{0x8000})
- }
- }
- out := Resize(10, 10, img, Lanczos3)
- for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ {
- for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ {
- color := out.At(x, y).(color.Gray16)
- if color.Y != 0x8000 {
- t.Errorf("%+v", color)
- }
- }
- }
-}
-
-func Test_Bounds(t *testing.T) {
- img := image.NewRGBA(image.Rect(20, 10, 200, 99))
- out := Resize(80, 80, img, Lanczos2)
- out.At(0, 0)
-}
-
-func Test_SameSizeReturnsOriginal(t *testing.T) {
- img := image.NewRGBA(image.Rect(0, 0, 10, 10))
- out := Resize(0, 0, img, Lanczos2)
-
- if img != out {
- t.Fail()
- }
-
- out = Resize(10, 10, img, Lanczos2)
-
- if img != out {
- t.Fail()
- }
-}
-
-func Test_PixelCoordinates(t *testing.T) {
- checkers := image.NewGray(image.Rect(0, 0, 4, 4))
- checkers.Pix = []uint8{
- 255, 0, 255, 0,
- 0, 255, 0, 255,
- 255, 0, 255, 0,
- 0, 255, 0, 255,
- }
-
- resized := Resize(12, 12, checkers, NearestNeighbor).(*image.Gray)
-
- if resized.Pix[0] != 255 || resized.Pix[1] != 255 || resized.Pix[2] != 255 {
- t.Fail()
- }
-
- if resized.Pix[3] != 0 || resized.Pix[4] != 0 || resized.Pix[5] != 0 {
- t.Fail()
- }
-}
-
-func Test_ResizeWithPremultipliedAlpha(t *testing.T) {
- img := image.NewRGBA(image.Rect(0, 0, 1, 4))
- for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
- // 0x80 = 0.5 * 0xFF.
- img.SetRGBA(0, y, color.RGBA{0x80, 0x80, 0x80, 0x80})
- }
-
- out := Resize(1, 2, img, MitchellNetravali)
-
- outputColor := out.At(0, 0).(color.NRGBA)
- if outputColor.R != 0xFF {
- t.Fail()
- }
-}
-
-const (
- // Use a small image size for benchmarks. We don't want memory performance
- // to affect the benchmark results.
- benchMaxX = 250
- benchMaxY = 250
-
- // Resize values near the original size require increase the amount of time
- // resize spends converting the image.
- benchWidth = 200
- benchHeight = 200
-)
-
-func benchRGBA(b *testing.B, interp InterpolationFunction) {
- m := image.NewRGBA(image.Rect(0, 0, benchMaxX, benchMaxY))
- // Initialize m's pixels to create a non-uniform image.
- for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ {
- for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ {
- i := m.PixOffset(x, y)
- m.Pix[i+0] = uint8(y + 4*x)
- m.Pix[i+1] = uint8(y + 4*x)
- m.Pix[i+2] = uint8(y + 4*x)
- m.Pix[i+3] = uint8(4*y + x)
- }
- }
-
- var out image.Image
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- out = Resize(benchWidth, benchHeight, m, interp)
- }
- out.At(0, 0)
-}
-
-// The names of some interpolation functions are truncated so that the columns
-// of 'go test -bench' line up.
-func Benchmark_Nearest_RGBA(b *testing.B) {
- benchRGBA(b, NearestNeighbor)
-}
-
-func Benchmark_Bilinear_RGBA(b *testing.B) {
- benchRGBA(b, Bilinear)
-}
-
-func Benchmark_Bicubic_RGBA(b *testing.B) {
- benchRGBA(b, Bicubic)
-}
-
-func Benchmark_Mitchell_RGBA(b *testing.B) {
- benchRGBA(b, MitchellNetravali)
-}
-
-func Benchmark_Lanczos2_RGBA(b *testing.B) {
- benchRGBA(b, Lanczos2)
-}
-
-func Benchmark_Lanczos3_RGBA(b *testing.B) {
- benchRGBA(b, Lanczos3)
-}
-
-func benchYCbCr(b *testing.B, interp InterpolationFunction) {
- m := image.NewYCbCr(image.Rect(0, 0, benchMaxX, benchMaxY), image.YCbCrSubsampleRatio422)
- // Initialize m's pixels to create a non-uniform image.
- for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ {
- for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ {
- yi := m.YOffset(x, y)
- ci := m.COffset(x, y)
- m.Y[yi] = uint8(16*y + x)
- m.Cb[ci] = uint8(y + 16*x)
- m.Cr[ci] = uint8(y + 16*x)
- }
- }
- var out image.Image
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- out = Resize(benchWidth, benchHeight, m, interp)
- }
- out.At(0, 0)
-}
-
-func Benchmark_Nearest_YCC(b *testing.B) {
- benchYCbCr(b, NearestNeighbor)
-}
-
-func Benchmark_Bilinear_YCC(b *testing.B) {
- benchYCbCr(b, Bilinear)
-}
-
-func Benchmark_Bicubic_YCC(b *testing.B) {
- benchYCbCr(b, Bicubic)
-}
-
-func Benchmark_Mitchell_YCC(b *testing.B) {
- benchYCbCr(b, MitchellNetravali)
-}
-
-func Benchmark_Lanczos2_YCC(b *testing.B) {
- benchYCbCr(b, Lanczos2)
-}
-
-func Benchmark_Lanczos3_YCC(b *testing.B) {
- benchYCbCr(b, Lanczos3)
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/thumbnail.go b/Godeps/_workspace/src/github.com/nfnt/resize/thumbnail.go
deleted file mode 100644
index 9efc246be..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/thumbnail.go
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
-Copyright (c) 2012, Jan Schlicht <jan.schlicht@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright notice
-and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-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.
-*/
-
-package resize
-
-import (
- "image"
-)
-
-// Thumbnail will downscale provided image to max width and height preserving
-// original aspect ratio and using the interpolation function interp.
-// It will return original image, without processing it, if original sizes
-// are already smaller than provided constraints.
-func Thumbnail(maxWidth, maxHeight uint, img image.Image, interp InterpolationFunction) image.Image {
- origBounds := img.Bounds()
- origWidth := uint(origBounds.Dx())
- origHeight := uint(origBounds.Dy())
- newWidth, newHeight := origWidth, origHeight
-
- // Return original image if it have same or smaller size as constraints
- if maxWidth >= origWidth && maxHeight >= origHeight {
- return img
- }
-
- // Preserve aspect ratio
- if origWidth > maxWidth {
- newHeight = uint(origHeight * maxWidth / origWidth)
- if newHeight < 1 {
- newHeight = 1
- }
- newWidth = maxWidth
- }
-
- if newHeight > maxHeight {
- newWidth = uint(newWidth * maxHeight / newHeight)
- if newWidth < 1 {
- newWidth = 1
- }
- newHeight = maxHeight
- }
- return Resize(newWidth, newHeight, img, interp)
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/thumbnail_test.go b/Godeps/_workspace/src/github.com/nfnt/resize/thumbnail_test.go
deleted file mode 100644
index bd9875b2b..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/thumbnail_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package resize
-
-import (
- "image"
- "runtime"
- "testing"
-)
-
-func init() {
- runtime.GOMAXPROCS(runtime.NumCPU())
-}
-
-var thumbnailTests = []struct {
- origWidth int
- origHeight int
- maxWidth uint
- maxHeight uint
- expectedWidth uint
- expectedHeight uint
-}{
- {5, 5, 10, 10, 5, 5},
- {10, 10, 5, 5, 5, 5},
- {10, 50, 10, 10, 2, 10},
- {50, 10, 10, 10, 10, 2},
- {50, 100, 60, 90, 45, 90},
- {120, 100, 60, 90, 60, 50},
- {200, 250, 200, 150, 120, 150},
-}
-
-func TestThumbnail(t *testing.T) {
- for i, tt := range thumbnailTests {
- img := image.NewGray16(image.Rect(0, 0, tt.origWidth, tt.origHeight))
-
- outImg := Thumbnail(tt.maxWidth, tt.maxHeight, img, NearestNeighbor)
-
- newWidth := uint(outImg.Bounds().Dx())
- newHeight := uint(outImg.Bounds().Dy())
- if newWidth != tt.expectedWidth ||
- newHeight != tt.expectedHeight {
- t.Errorf("%d. Thumbnail(%v, %v, img, NearestNeighbor) => "+
- "width: %v, height: %v, want width: %v, height: %v",
- i, tt.maxWidth, tt.maxHeight,
- newWidth, newHeight, tt.expectedWidth, tt.expectedHeight,
- )
- }
- }
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/ycc.go b/Godeps/_workspace/src/github.com/nfnt/resize/ycc.go
deleted file mode 100644
index 104159955..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/ycc.go
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
-Copyright (c) 2014, Charlie Vieth <charlie.vieth@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright notice
-and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-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.
-*/
-
-package resize
-
-import (
- "image"
- "image/color"
-)
-
-// ycc is an in memory YCbCr image. The Y, Cb and Cr samples are held in a
-// single slice to increase resizing performance.
-type ycc struct {
- // Pix holds the image's pixels, in Y, Cb, Cr order. The pixel at
- // (x, y) starts at Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*3].
- Pix []uint8
- // Stride is the Pix stride (in bytes) between vertically adjacent pixels.
- Stride int
- // Rect is the image's bounds.
- Rect image.Rectangle
- // SubsampleRatio is the subsample ratio of the original YCbCr image.
- SubsampleRatio image.YCbCrSubsampleRatio
-}
-
-// PixOffset returns the index of the first element of Pix that corresponds to
-// the pixel at (x, y).
-func (p *ycc) PixOffset(x, y int) int {
- return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*3
-}
-
-func (p *ycc) Bounds() image.Rectangle {
- return p.Rect
-}
-
-func (p *ycc) ColorModel() color.Model {
- return color.YCbCrModel
-}
-
-func (p *ycc) At(x, y int) color.Color {
- if !(image.Point{x, y}.In(p.Rect)) {
- return color.YCbCr{}
- }
- i := p.PixOffset(x, y)
- return color.YCbCr{
- p.Pix[i+0],
- p.Pix[i+1],
- p.Pix[i+2],
- }
-}
-
-func (p *ycc) Opaque() bool {
- return true
-}
-
-// SubImage returns an image representing the portion of the image p visible
-// through r. The returned value shares pixels with the original image.
-func (p *ycc) SubImage(r image.Rectangle) image.Image {
- r = r.Intersect(p.Rect)
- if r.Empty() {
- return &ycc{SubsampleRatio: p.SubsampleRatio}
- }
- i := p.PixOffset(r.Min.X, r.Min.Y)
- return &ycc{
- Pix: p.Pix[i:],
- Stride: p.Stride,
- Rect: r,
- SubsampleRatio: p.SubsampleRatio,
- }
-}
-
-// newYCC returns a new ycc with the given bounds and subsample ratio.
-func newYCC(r image.Rectangle, s image.YCbCrSubsampleRatio) *ycc {
- w, h := r.Dx(), r.Dy()
- buf := make([]uint8, 3*w*h)
- return &ycc{Pix: buf, Stride: 3 * w, Rect: r, SubsampleRatio: s}
-}
-
-// YCbCr converts ycc to a YCbCr image with the same subsample ratio
-// as the YCbCr image that ycc was generated from.
-func (p *ycc) YCbCr() *image.YCbCr {
- ycbcr := image.NewYCbCr(p.Rect, p.SubsampleRatio)
- var off int
-
- switch ycbcr.SubsampleRatio {
- case image.YCbCrSubsampleRatio422:
- for y := ycbcr.Rect.Min.Y; y < ycbcr.Rect.Max.Y; y++ {
- yy := (y - ycbcr.Rect.Min.Y) * ycbcr.YStride
- cy := (y - ycbcr.Rect.Min.Y) * ycbcr.CStride
- for x := ycbcr.Rect.Min.X; x < ycbcr.Rect.Max.X; x++ {
- xx := (x - ycbcr.Rect.Min.X)
- yi := yy + xx
- ci := cy + xx/2
- ycbcr.Y[yi] = p.Pix[off+0]
- ycbcr.Cb[ci] = p.Pix[off+1]
- ycbcr.Cr[ci] = p.Pix[off+2]
- off += 3
- }
- }
- case image.YCbCrSubsampleRatio420:
- for y := ycbcr.Rect.Min.Y; y < ycbcr.Rect.Max.Y; y++ {
- yy := (y - ycbcr.Rect.Min.Y) * ycbcr.YStride
- cy := (y/2 - ycbcr.Rect.Min.Y/2) * ycbcr.CStride
- for x := ycbcr.Rect.Min.X; x < ycbcr.Rect.Max.X; x++ {
- xx := (x - ycbcr.Rect.Min.X)
- yi := yy + xx
- ci := cy + xx/2
- ycbcr.Y[yi] = p.Pix[off+0]
- ycbcr.Cb[ci] = p.Pix[off+1]
- ycbcr.Cr[ci] = p.Pix[off+2]
- off += 3
- }
- }
- case image.YCbCrSubsampleRatio440:
- for y := ycbcr.Rect.Min.Y; y < ycbcr.Rect.Max.Y; y++ {
- yy := (y - ycbcr.Rect.Min.Y) * ycbcr.YStride
- cy := (y/2 - ycbcr.Rect.Min.Y/2) * ycbcr.CStride
- for x := ycbcr.Rect.Min.X; x < ycbcr.Rect.Max.X; x++ {
- xx := (x - ycbcr.Rect.Min.X)
- yi := yy + xx
- ci := cy + xx
- ycbcr.Y[yi] = p.Pix[off+0]
- ycbcr.Cb[ci] = p.Pix[off+1]
- ycbcr.Cr[ci] = p.Pix[off+2]
- off += 3
- }
- }
- default:
- // Default to 4:4:4 subsampling.
- for y := ycbcr.Rect.Min.Y; y < ycbcr.Rect.Max.Y; y++ {
- yy := (y - ycbcr.Rect.Min.Y) * ycbcr.YStride
- cy := (y - ycbcr.Rect.Min.Y) * ycbcr.CStride
- for x := ycbcr.Rect.Min.X; x < ycbcr.Rect.Max.X; x++ {
- xx := (x - ycbcr.Rect.Min.X)
- yi := yy + xx
- ci := cy + xx
- ycbcr.Y[yi] = p.Pix[off+0]
- ycbcr.Cb[ci] = p.Pix[off+1]
- ycbcr.Cr[ci] = p.Pix[off+2]
- off += 3
- }
- }
- }
- return ycbcr
-}
-
-// imageYCbCrToYCC converts a YCbCr image to a ycc image for resizing.
-func imageYCbCrToYCC(in *image.YCbCr) *ycc {
- w, h := in.Rect.Dx(), in.Rect.Dy()
- r := image.Rect(0, 0, w, h)
- buf := make([]uint8, 3*w*h)
- p := ycc{Pix: buf, Stride: 3 * w, Rect: r, SubsampleRatio: in.SubsampleRatio}
- var off int
-
- switch in.SubsampleRatio {
- case image.YCbCrSubsampleRatio422:
- for y := in.Rect.Min.Y; y < in.Rect.Max.Y; y++ {
- yy := (y - in.Rect.Min.Y) * in.YStride
- cy := (y - in.Rect.Min.Y) * in.CStride
- for x := in.Rect.Min.X; x < in.Rect.Max.X; x++ {
- xx := (x - in.Rect.Min.X)
- yi := yy + xx
- ci := cy + xx/2
- p.Pix[off+0] = in.Y[yi]
- p.Pix[off+1] = in.Cb[ci]
- p.Pix[off+2] = in.Cr[ci]
- off += 3
- }
- }
- case image.YCbCrSubsampleRatio420:
- for y := in.Rect.Min.Y; y < in.Rect.Max.Y; y++ {
- yy := (y - in.Rect.Min.Y) * in.YStride
- cy := (y/2 - in.Rect.Min.Y/2) * in.CStride
- for x := in.Rect.Min.X; x < in.Rect.Max.X; x++ {
- xx := (x - in.Rect.Min.X)
- yi := yy + xx
- ci := cy + xx/2
- p.Pix[off+0] = in.Y[yi]
- p.Pix[off+1] = in.Cb[ci]
- p.Pix[off+2] = in.Cr[ci]
- off += 3
- }
- }
- case image.YCbCrSubsampleRatio440:
- for y := in.Rect.Min.Y; y < in.Rect.Max.Y; y++ {
- yy := (y - in.Rect.Min.Y) * in.YStride
- cy := (y/2 - in.Rect.Min.Y/2) * in.CStride
- for x := in.Rect.Min.X; x < in.Rect.Max.X; x++ {
- xx := (x - in.Rect.Min.X)
- yi := yy + xx
- ci := cy + xx
- p.Pix[off+0] = in.Y[yi]
- p.Pix[off+1] = in.Cb[ci]
- p.Pix[off+2] = in.Cr[ci]
- off += 3
- }
- }
- default:
- // Default to 4:4:4 subsampling.
- for y := in.Rect.Min.Y; y < in.Rect.Max.Y; y++ {
- yy := (y - in.Rect.Min.Y) * in.YStride
- cy := (y - in.Rect.Min.Y) * in.CStride
- for x := in.Rect.Min.X; x < in.Rect.Max.X; x++ {
- xx := (x - in.Rect.Min.X)
- yi := yy + xx
- ci := cy + xx
- p.Pix[off+0] = in.Y[yi]
- p.Pix[off+1] = in.Cb[ci]
- p.Pix[off+2] = in.Cr[ci]
- off += 3
- }
- }
- }
- return &p
-}
diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/ycc_test.go b/Godeps/_workspace/src/github.com/nfnt/resize/ycc_test.go
deleted file mode 100644
index 54d53d157..000000000
--- a/Godeps/_workspace/src/github.com/nfnt/resize/ycc_test.go
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
-Copyright (c) 2014, Charlie Vieth <charlie.vieth@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright notice
-and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-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.
-*/
-
-package resize
-
-import (
- "image"
- "image/color"
- "testing"
-)
-
-type Image interface {
- image.Image
- SubImage(image.Rectangle) image.Image
-}
-
-func TestImage(t *testing.T) {
- testImage := []Image{
- newYCC(image.Rect(0, 0, 10, 10), image.YCbCrSubsampleRatio420),
- newYCC(image.Rect(0, 0, 10, 10), image.YCbCrSubsampleRatio422),
- newYCC(image.Rect(0, 0, 10, 10), image.YCbCrSubsampleRatio440),
- newYCC(image.Rect(0, 0, 10, 10), image.YCbCrSubsampleRatio444),
- }
- for _, m := range testImage {
- if !image.Rect(0, 0, 10, 10).Eq(m.Bounds()) {
- t.Errorf("%T: want bounds %v, got %v",
- m, image.Rect(0, 0, 10, 10), m.Bounds())
- continue
- }
- m = m.SubImage(image.Rect(3, 2, 9, 8)).(Image)
- if !image.Rect(3, 2, 9, 8).Eq(m.Bounds()) {
- t.Errorf("%T: sub-image want bounds %v, got %v",
- m, image.Rect(3, 2, 9, 8), m.Bounds())
- continue
- }
- // Test that taking an empty sub-image starting at a corner does not panic.
- m.SubImage(image.Rect(0, 0, 0, 0))
- m.SubImage(image.Rect(10, 0, 10, 0))
- m.SubImage(image.Rect(0, 10, 0, 10))
- m.SubImage(image.Rect(10, 10, 10, 10))
- }
-}
-
-func TestConvertYCbCr(t *testing.T) {
- testImage := []Image{
- image.NewYCbCr(image.Rect(0, 0, 50, 50), image.YCbCrSubsampleRatio420),
- image.NewYCbCr(image.Rect(0, 0, 50, 50), image.YCbCrSubsampleRatio422),
- image.NewYCbCr(image.Rect(0, 0, 50, 50), image.YCbCrSubsampleRatio440),
- image.NewYCbCr(image.Rect(0, 0, 50, 50), image.YCbCrSubsampleRatio444),
- }
-
- for _, img := range testImage {
- m := img.(*image.YCbCr)
- for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ {
- for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ {
- yi := m.YOffset(x, y)
- ci := m.COffset(x, y)
- m.Y[yi] = uint8(16*y + x)
- m.Cb[ci] = uint8(y + 16*x)
- m.Cr[ci] = uint8(y + 16*x)
- }
- }
-
- // test conversion from YCbCr to ycc
- yc := imageYCbCrToYCC(m)
- for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ {
- for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ {
- ystride := 3 * (m.Rect.Max.X - m.Rect.Min.X)
- xstride := 3
- yi := m.YOffset(x, y)
- ci := m.COffset(x, y)
- si := (y * ystride) + (x * xstride)
- if m.Y[yi] != yc.Pix[si] {
- t.Errorf("Err Y - found: %d expected: %d x: %d y: %d yi: %d si: %d",
- m.Y[yi], yc.Pix[si], x, y, yi, si)
- }
- if m.Cb[ci] != yc.Pix[si+1] {
- t.Errorf("Err Cb - found: %d expected: %d x: %d y: %d ci: %d si: %d",
- m.Cb[ci], yc.Pix[si+1], x, y, ci, si+1)
- }
- if m.Cr[ci] != yc.Pix[si+2] {
- t.Errorf("Err Cr - found: %d expected: %d x: %d y: %d ci: %d si: %d",
- m.Cr[ci], yc.Pix[si+2], x, y, ci, si+2)
- }
- }
- }
-
- // test conversion from ycc back to YCbCr
- ym := yc.YCbCr()
- for y := m.Rect.Min.Y; y < m.Rect.Max.Y; y++ {
- for x := m.Rect.Min.X; x < m.Rect.Max.X; x++ {
- yi := m.YOffset(x, y)
- ci := m.COffset(x, y)
- if m.Y[yi] != ym.Y[yi] {
- t.Errorf("Err Y - found: %d expected: %d x: %d y: %d yi: %d",
- m.Y[yi], ym.Y[yi], x, y, yi)
- }
- if m.Cb[ci] != ym.Cb[ci] {
- t.Errorf("Err Cb - found: %d expected: %d x: %d y: %d ci: %d",
- m.Cb[ci], ym.Cb[ci], x, y, ci)
- }
- if m.Cr[ci] != ym.Cr[ci] {
- t.Errorf("Err Cr - found: %d expected: %d x: %d y: %d ci: %d",
- m.Cr[ci], ym.Cr[ci], x, y, ci)
- }
- }
- }
- }
-}
-
-func TestYCbCr(t *testing.T) {
- rects := []image.Rectangle{
- image.Rect(0, 0, 16, 16),
- image.Rect(1, 0, 16, 16),
- image.Rect(0, 1, 16, 16),
- image.Rect(1, 1, 16, 16),
- image.Rect(1, 1, 15, 16),
- image.Rect(1, 1, 16, 15),
- image.Rect(1, 1, 15, 15),
- image.Rect(2, 3, 14, 15),
- image.Rect(7, 0, 7, 16),
- image.Rect(0, 8, 16, 8),
- image.Rect(0, 0, 10, 11),
- image.Rect(5, 6, 16, 16),
- image.Rect(7, 7, 8, 8),
- image.Rect(7, 8, 8, 9),
- image.Rect(8, 7, 9, 8),
- image.Rect(8, 8, 9, 9),
- image.Rect(7, 7, 17, 17),
- image.Rect(8, 8, 17, 17),
- image.Rect(9, 9, 17, 17),
- image.Rect(10, 10, 17, 17),
- }
- subsampleRatios := []image.YCbCrSubsampleRatio{
- image.YCbCrSubsampleRatio444,
- image.YCbCrSubsampleRatio422,
- image.YCbCrSubsampleRatio420,
- image.YCbCrSubsampleRatio440,
- }
- deltas := []image.Point{
- image.Pt(0, 0),
- image.Pt(1000, 1001),
- image.Pt(5001, -400),
- image.Pt(-701, -801),
- }
- for _, r := range rects {
- for _, subsampleRatio := range subsampleRatios {
- for _, delta := range deltas {
- testYCbCr(t, r, subsampleRatio, delta)
- }
- }
- if testing.Short() {
- break
- }
- }
-}
-
-func testYCbCr(t *testing.T, r image.Rectangle, subsampleRatio image.YCbCrSubsampleRatio, delta image.Point) {
- // Create a YCbCr image m, whose bounds are r translated by (delta.X, delta.Y).
- r1 := r.Add(delta)
- img := image.NewYCbCr(r1, subsampleRatio)
-
- // Initialize img's pixels. For 422 and 420 subsampling, some of the Cb and Cr elements
- // will be set multiple times. That's OK. We just want to avoid a uniform image.
- for y := r1.Min.Y; y < r1.Max.Y; y++ {
- for x := r1.Min.X; x < r1.Max.X; x++ {
- yi := img.YOffset(x, y)
- ci := img.COffset(x, y)
- img.Y[yi] = uint8(16*y + x)
- img.Cb[ci] = uint8(y + 16*x)
- img.Cr[ci] = uint8(y + 16*x)
- }
- }
-
- m := imageYCbCrToYCC(img)
-
- // Make various sub-images of m.
- for y0 := delta.Y + 3; y0 < delta.Y+7; y0++ {
- for y1 := delta.Y + 8; y1 < delta.Y+13; y1++ {
- for x0 := delta.X + 3; x0 < delta.X+7; x0++ {
- for x1 := delta.X + 8; x1 < delta.X+13; x1++ {
- subRect := image.Rect(x0, y0, x1, y1)
- sub := m.SubImage(subRect).(*ycc)
-
- // For each point in the sub-image's bounds, check that m.At(x, y) equals sub.At(x, y).
- for y := sub.Rect.Min.Y; y < sub.Rect.Max.Y; y++ {
- for x := sub.Rect.Min.X; x < sub.Rect.Max.X; x++ {
- color0 := m.At(x, y).(color.YCbCr)
- color1 := sub.At(x, y).(color.YCbCr)
- if color0 != color1 {
- t.Errorf("r=%v, subsampleRatio=%v, delta=%v, x=%d, y=%d, color0=%v, color1=%v",
- r, subsampleRatio, delta, x, y, color0, color1)
- return
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/Godeps/_workspace/src/golang.org/x/image/tiff/buffer.go b/Godeps/_workspace/src/golang.org/x/image/tiff/buffer.go
new file mode 100644
index 000000000..d1801be48
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/image/tiff/buffer.go
@@ -0,0 +1,69 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tiff
+
+import "io"
+
+// buffer buffers an io.Reader to satisfy io.ReaderAt.
+type buffer struct {
+ r io.Reader
+ buf []byte
+}
+
+// fill reads data from b.r until the buffer contains at least end bytes.
+func (b *buffer) fill(end int) error {
+ m := len(b.buf)
+ if end > m {
+ if end > cap(b.buf) {
+ newcap := 1024
+ for newcap < end {
+ newcap *= 2
+ }
+ newbuf := make([]byte, end, newcap)
+ copy(newbuf, b.buf)
+ b.buf = newbuf
+ } else {
+ b.buf = b.buf[:end]
+ }
+ if n, err := io.ReadFull(b.r, b.buf[m:end]); err != nil {
+ end = m + n
+ b.buf = b.buf[:end]
+ return err
+ }
+ }
+ return nil
+}
+
+func (b *buffer) ReadAt(p []byte, off int64) (int, error) {
+ o := int(off)
+ end := o + len(p)
+ if int64(end) != off+int64(len(p)) {
+ return 0, io.ErrUnexpectedEOF
+ }
+
+ err := b.fill(end)
+ return copy(p, b.buf[o:end]), err
+}
+
+// Slice returns a slice of the underlying buffer. The slice contains
+// n bytes starting at offset off.
+func (b *buffer) Slice(off, n int) ([]byte, error) {
+ end := off + n
+ if err := b.fill(end); err != nil {
+ return nil, err
+ }
+ return b.buf[off:end], nil
+}
+
+// newReaderAt converts an io.Reader into an io.ReaderAt.
+func newReaderAt(r io.Reader) io.ReaderAt {
+ if ra, ok := r.(io.ReaderAt); ok {
+ return ra
+ }
+ return &buffer{
+ r: r,
+ buf: make([]byte, 0, 1024),
+ }
+}
diff --git a/Godeps/_workspace/src/golang.org/x/image/tiff/compress.go b/Godeps/_workspace/src/golang.org/x/image/tiff/compress.go
new file mode 100644
index 000000000..3f176f00a
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/image/tiff/compress.go
@@ -0,0 +1,58 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tiff
+
+import (
+ "bufio"
+ "io"
+)
+
+type byteReader interface {
+ io.Reader
+ io.ByteReader
+}
+
+// unpackBits decodes the PackBits-compressed data in src and returns the
+// uncompressed data.
+//
+// The PackBits compression format is described in section 9 (p. 42)
+// of the TIFF spec.
+func unpackBits(r io.Reader) ([]byte, error) {
+ buf := make([]byte, 128)
+ dst := make([]byte, 0, 1024)
+ br, ok := r.(byteReader)
+ if !ok {
+ br = bufio.NewReader(r)
+ }
+
+ for {
+ b, err := br.ReadByte()
+ if err != nil {
+ if err == io.EOF {
+ return dst, nil
+ }
+ return nil, err
+ }
+ code := int(int8(b))
+ switch {
+ case code >= 0:
+ n, err := io.ReadFull(br, buf[:code+1])
+ if err != nil {
+ return nil, err
+ }
+ dst = append(dst, buf[:n]...)
+ case code == -128:
+ // No-op.
+ default:
+ if b, err = br.ReadByte(); err != nil {
+ return nil, err
+ }
+ for j := 0; j < 1-code; j++ {
+ buf[j] = b
+ }
+ dst = append(dst, buf[:1-code]...)
+ }
+ }
+}
diff --git a/Godeps/_workspace/src/golang.org/x/image/tiff/consts.go b/Godeps/_workspace/src/golang.org/x/image/tiff/consts.go
new file mode 100644
index 000000000..3c51a70be
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/image/tiff/consts.go
@@ -0,0 +1,133 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tiff
+
+// A tiff image file contains one or more images. The metadata
+// of each image is contained in an Image File Directory (IFD),
+// which contains entries of 12 bytes each and is described
+// on page 14-16 of the specification. An IFD entry consists of
+//
+// - a tag, which describes the signification of the entry,
+// - the data type and length of the entry,
+// - the data itself or a pointer to it if it is more than 4 bytes.
+//
+// The presence of a length means that each IFD is effectively an array.
+
+const (
+ leHeader = "II\x2A\x00" // Header for little-endian files.
+ beHeader = "MM\x00\x2A" // Header for big-endian files.
+
+ ifdLen = 12 // Length of an IFD entry in bytes.
+)
+
+// Data types (p. 14-16 of the spec).
+const (
+ dtByte = 1
+ dtASCII = 2
+ dtShort = 3
+ dtLong = 4
+ dtRational = 5
+)
+
+// The length of one instance of each data type in bytes.
+var lengths = [...]uint32{0, 1, 1, 2, 4, 8}
+
+// Tags (see p. 28-41 of the spec).
+const (
+ tImageWidth = 256
+ tImageLength = 257
+ tBitsPerSample = 258
+ tCompression = 259
+ tPhotometricInterpretation = 262
+
+ tStripOffsets = 273
+ tSamplesPerPixel = 277
+ tRowsPerStrip = 278
+ tStripByteCounts = 279
+
+ tTileWidth = 322
+ tTileLength = 323
+ tTileOffsets = 324
+ tTileByteCounts = 325
+
+ tXResolution = 282
+ tYResolution = 283
+ tResolutionUnit = 296
+
+ tPredictor = 317
+ tColorMap = 320
+ tExtraSamples = 338
+ tSampleFormat = 339
+)
+
+// Compression types (defined in various places in the spec and supplements).
+const (
+ cNone = 1
+ cCCITT = 2
+ cG3 = 3 // Group 3 Fax.
+ cG4 = 4 // Group 4 Fax.
+ cLZW = 5
+ cJPEGOld = 6 // Superseded by cJPEG.
+ cJPEG = 7
+ cDeflate = 8 // zlib compression.
+ cPackBits = 32773
+ cDeflateOld = 32946 // Superseded by cDeflate.
+)
+
+// Photometric interpretation values (see p. 37 of the spec).
+const (
+ pWhiteIsZero = 0
+ pBlackIsZero = 1
+ pRGB = 2
+ pPaletted = 3
+ pTransMask = 4 // transparency mask
+ pCMYK = 5
+ pYCbCr = 6
+ pCIELab = 8
+)
+
+// Values for the tPredictor tag (page 64-65 of the spec).
+const (
+ prNone = 1
+ prHorizontal = 2
+)
+
+// Values for the tResolutionUnit tag (page 18).
+const (
+ resNone = 1
+ resPerInch = 2 // Dots per inch.
+ resPerCM = 3 // Dots per centimeter.
+)
+
+// imageMode represents the mode of the image.
+type imageMode int
+
+const (
+ mBilevel imageMode = iota
+ mPaletted
+ mGray
+ mGrayInvert
+ mRGB
+ mRGBA
+ mNRGBA
+)
+
+// CompressionType describes the type of compression used in Options.
+type CompressionType int
+
+const (
+ Uncompressed CompressionType = iota
+ Deflate
+)
+
+// specValue returns the compression type constant from the TIFF spec that
+// is equivalent to c.
+func (c CompressionType) specValue() uint32 {
+ switch c {
+ case Deflate:
+ return cDeflate
+ }
+ return cNone
+}
diff --git a/Godeps/_workspace/src/golang.org/x/image/tiff/lzw/reader.go b/Godeps/_workspace/src/golang.org/x/image/tiff/lzw/reader.go
new file mode 100644
index 000000000..dc9f7dd1d
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/image/tiff/lzw/reader.go
@@ -0,0 +1,277 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package lzw implements the Lempel-Ziv-Welch compressed data format,
+// described in T. A. Welch, ``A Technique for High-Performance Data
+// Compression'', Computer, 17(6) (June 1984), pp 8-19.
+//
+// In particular, it implements LZW as used by the TIFF file format, including
+// an "off by one" algorithmic difference when compared to standard LZW.
+package lzw
+
+/*
+This file was branched from src/pkg/compress/lzw/reader.go in the
+standard library. Differences from the original are marked with "NOTE".
+
+The tif_lzw.c file in the libtiff C library has this comment:
+
+----
+The 5.0 spec describes a different algorithm than Aldus
+implements. Specifically, Aldus does code length transitions
+one code earlier than should be done (for real LZW).
+Earlier versions of this library implemented the correct
+LZW algorithm, but emitted codes in a bit order opposite
+to the TIFF spec. Thus, to maintain compatibility w/ Aldus
+we interpret MSB-LSB ordered codes to be images written w/
+old versions of this library, but otherwise adhere to the
+Aldus "off by one" algorithm.
+----
+
+The Go code doesn't read (invalid) TIFF files written by old versions of
+libtiff, but the LZW algorithm in this package still differs from the one in
+Go's standard package library to accomodate this "off by one" in valid TIFFs.
+*/
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "io"
+)
+
+// Order specifies the bit ordering in an LZW data stream.
+type Order int
+
+const (
+ // LSB means Least Significant Bits first, as used in the GIF file format.
+ LSB Order = iota
+ // MSB means Most Significant Bits first, as used in the TIFF and PDF
+ // file formats.
+ MSB
+)
+
+const (
+ maxWidth = 12
+ decoderInvalidCode = 0xffff
+ flushBuffer = 1 << maxWidth
+)
+
+// decoder is the state from which the readXxx method converts a byte
+// stream into a code stream.
+type decoder struct {
+ r io.ByteReader
+ bits uint32
+ nBits uint
+ width uint
+ read func(*decoder) (uint16, error) // readLSB or readMSB
+ litWidth int // width in bits of literal codes
+ err error
+
+ // The first 1<<litWidth codes are literal codes.
+ // The next two codes mean clear and EOF.
+ // Other valid codes are in the range [lo, hi] where lo := clear + 2,
+ // with the upper bound incrementing on each code seen.
+ // overflow is the code at which hi overflows the code width. NOTE: TIFF's LZW is "off by one".
+ // last is the most recently seen code, or decoderInvalidCode.
+ clear, eof, hi, overflow, last uint16
+
+ // Each code c in [lo, hi] expands to two or more bytes. For c != hi:
+ // suffix[c] is the last of these bytes.
+ // prefix[c] is the code for all but the last byte.
+ // This code can either be a literal code or another code in [lo, c).
+ // The c == hi case is a special case.
+ suffix [1 << maxWidth]uint8
+ prefix [1 << maxWidth]uint16
+
+ // output is the temporary output buffer.
+ // Literal codes are accumulated from the start of the buffer.
+ // Non-literal codes decode to a sequence of suffixes that are first
+ // written right-to-left from the end of the buffer before being copied
+ // to the start of the buffer.
+ // It is flushed when it contains >= 1<<maxWidth bytes,
+ // so that there is always room to decode an entire code.
+ output [2 * 1 << maxWidth]byte
+ o int // write index into output
+ toRead []byte // bytes to return from Read
+}
+
+// readLSB returns the next code for "Least Significant Bits first" data.
+func (d *decoder) readLSB() (uint16, error) {
+ for d.nBits < d.width {
+ x, err := d.r.ReadByte()
+ if err != nil {
+ return 0, err
+ }
+ d.bits |= uint32(x) << d.nBits
+ d.nBits += 8
+ }
+ code := uint16(d.bits & (1<<d.width - 1))
+ d.bits >>= d.width
+ d.nBits -= d.width
+ return code, nil
+}
+
+// readMSB returns the next code for "Most Significant Bits first" data.
+func (d *decoder) readMSB() (uint16, error) {
+ for d.nBits < d.width {
+ x, err := d.r.ReadByte()
+ if err != nil {
+ return 0, err
+ }
+ d.bits |= uint32(x) << (24 - d.nBits)
+ d.nBits += 8
+ }
+ code := uint16(d.bits >> (32 - d.width))
+ d.bits <<= d.width
+ d.nBits -= d.width
+ return code, nil
+}
+
+func (d *decoder) Read(b []byte) (int, error) {
+ for {
+ if len(d.toRead) > 0 {
+ n := copy(b, d.toRead)
+ d.toRead = d.toRead[n:]
+ return n, nil
+ }
+ if d.err != nil {
+ return 0, d.err
+ }
+ d.decode()
+ }
+}
+
+// decode decompresses bytes from r and leaves them in d.toRead.
+// read specifies how to decode bytes into codes.
+// litWidth is the width in bits of literal codes.
+func (d *decoder) decode() {
+ // Loop over the code stream, converting codes into decompressed bytes.
+ for {
+ code, err := d.read(d)
+ if err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ d.err = err
+ d.flush()
+ return
+ }
+ switch {
+ case code < d.clear:
+ // We have a literal code.
+ d.output[d.o] = uint8(code)
+ d.o++
+ if d.last != decoderInvalidCode {
+ // Save what the hi code expands to.
+ d.suffix[d.hi] = uint8(code)
+ d.prefix[d.hi] = d.last
+ }
+ case code == d.clear:
+ d.width = 1 + uint(d.litWidth)
+ d.hi = d.eof
+ d.overflow = 1 << d.width
+ d.last = decoderInvalidCode
+ continue
+ case code == d.eof:
+ d.flush()
+ d.err = io.EOF
+ return
+ case code <= d.hi:
+ c, i := code, len(d.output)-1
+ if code == d.hi {
+ // code == hi is a special case which expands to the last expansion
+ // followed by the head of the last expansion. To find the head, we walk
+ // the prefix chain until we find a literal code.
+ c = d.last
+ for c >= d.clear {
+ c = d.prefix[c]
+ }
+ d.output[i] = uint8(c)
+ i--
+ c = d.last
+ }
+ // Copy the suffix chain into output and then write that to w.
+ for c >= d.clear {
+ d.output[i] = d.suffix[c]
+ i--
+ c = d.prefix[c]
+ }
+ d.output[i] = uint8(c)
+ d.o += copy(d.output[d.o:], d.output[i:])
+ if d.last != decoderInvalidCode {
+ // Save what the hi code expands to.
+ d.suffix[d.hi] = uint8(c)
+ d.prefix[d.hi] = d.last
+ }
+ default:
+ d.err = errors.New("lzw: invalid code")
+ d.flush()
+ return
+ }
+ d.last, d.hi = code, d.hi+1
+ if d.hi+1 >= d.overflow { // NOTE: the "+1" is where TIFF's LZW differs from the standard algorithm.
+ if d.width == maxWidth {
+ d.last = decoderInvalidCode
+ } else {
+ d.width++
+ d.overflow <<= 1
+ }
+ }
+ if d.o >= flushBuffer {
+ d.flush()
+ return
+ }
+ }
+}
+
+func (d *decoder) flush() {
+ d.toRead = d.output[:d.o]
+ d.o = 0
+}
+
+var errClosed = errors.New("lzw: reader/writer is closed")
+
+func (d *decoder) Close() error {
+ d.err = errClosed // in case any Reads come along
+ return nil
+}
+
+// NewReader creates a new io.ReadCloser.
+// Reads from the returned io.ReadCloser read and decompress data from r.
+// If r does not also implement io.ByteReader,
+// the decompressor may read more data than necessary from r.
+// It is the caller's responsibility to call Close on the ReadCloser when
+// finished reading.
+// The number of bits to use for literal codes, litWidth, must be in the
+// range [2,8] and is typically 8. It must equal the litWidth
+// used during compression.
+func NewReader(r io.Reader, order Order, litWidth int) io.ReadCloser {
+ d := new(decoder)
+ switch order {
+ case LSB:
+ d.read = (*decoder).readLSB
+ case MSB:
+ d.read = (*decoder).readMSB
+ default:
+ d.err = errors.New("lzw: unknown order")
+ return d
+ }
+ if litWidth < 2 || 8 < litWidth {
+ d.err = fmt.Errorf("lzw: litWidth %d out of range", litWidth)
+ return d
+ }
+ if br, ok := r.(io.ByteReader); ok {
+ d.r = br
+ } else {
+ d.r = bufio.NewReader(r)
+ }
+ d.litWidth = litWidth
+ d.width = 1 + uint(litWidth)
+ d.clear = uint16(1) << uint(litWidth)
+ d.eof, d.hi = d.clear+1, d.clear+1
+ d.overflow = uint16(1) << d.width
+ d.last = decoderInvalidCode
+
+ return d
+}
diff --git a/Godeps/_workspace/src/golang.org/x/image/tiff/reader.go b/Godeps/_workspace/src/golang.org/x/image/tiff/reader.go
new file mode 100644
index 000000000..714e3dda7
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/image/tiff/reader.go
@@ -0,0 +1,681 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package tiff implements a TIFF image decoder and encoder.
+//
+// The TIFF specification is at http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf
+package tiff
+
+import (
+ "compress/zlib"
+ "encoding/binary"
+ "fmt"
+ "image"
+ "image/color"
+ "io"
+ "io/ioutil"
+ "math"
+
+ "golang.org/x/image/tiff/lzw"
+)
+
+// A FormatError reports that the input is not a valid TIFF image.
+type FormatError string
+
+func (e FormatError) Error() string {
+ return "tiff: invalid format: " + string(e)
+}
+
+// An UnsupportedError reports that the input uses a valid but
+// unimplemented feature.
+type UnsupportedError string
+
+func (e UnsupportedError) Error() string {
+ return "tiff: unsupported feature: " + string(e)
+}
+
+// An InternalError reports that an internal error was encountered.
+type InternalError string
+
+func (e InternalError) Error() string {
+ return "tiff: internal error: " + string(e)
+}
+
+var errNoPixels = FormatError("not enough pixel data")
+
+type decoder struct {
+ r io.ReaderAt
+ byteOrder binary.ByteOrder
+ config image.Config
+ mode imageMode
+ bpp uint
+ features map[int][]uint
+ palette []color.Color
+
+ buf []byte
+ off int // Current offset in buf.
+ v uint32 // Buffer value for reading with arbitrary bit depths.
+ nbits uint // Remaining number of bits in v.
+}
+
+// firstVal returns the first uint of the features entry with the given tag,
+// or 0 if the tag does not exist.
+func (d *decoder) firstVal(tag int) uint {
+ f := d.features[tag]
+ if len(f) == 0 {
+ return 0
+ }
+ return f[0]
+}
+
+// ifdUint decodes the IFD entry in p, which must be of the Byte, Short
+// or Long type, and returns the decoded uint values.
+func (d *decoder) ifdUint(p []byte) (u []uint, err error) {
+ var raw []byte
+ if len(p) < ifdLen {
+ return nil, FormatError("bad IFD entry")
+ }
+
+ datatype := d.byteOrder.Uint16(p[2:4])
+ if dt := int(datatype); dt <= 0 || dt >= len(lengths) {
+ return nil, UnsupportedError("IFD entry datatype")
+ }
+
+ count := d.byteOrder.Uint32(p[4:8])
+ if count > math.MaxInt32/lengths[datatype] {
+ return nil, FormatError("IFD data too large")
+ }
+ if datalen := lengths[datatype] * count; datalen > 4 {
+ // The IFD contains a pointer to the real value.
+ raw = make([]byte, datalen)
+ _, err = d.r.ReadAt(raw, int64(d.byteOrder.Uint32(p[8:12])))
+ } else {
+ raw = p[8 : 8+datalen]
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ u = make([]uint, count)
+ switch datatype {
+ case dtByte:
+ for i := uint32(0); i < count; i++ {
+ u[i] = uint(raw[i])
+ }
+ case dtShort:
+ for i := uint32(0); i < count; i++ {
+ u[i] = uint(d.byteOrder.Uint16(raw[2*i : 2*(i+1)]))
+ }
+ case dtLong:
+ for i := uint32(0); i < count; i++ {
+ u[i] = uint(d.byteOrder.Uint32(raw[4*i : 4*(i+1)]))
+ }
+ default:
+ return nil, UnsupportedError("data type")
+ }
+ return u, nil
+}
+
+// parseIFD decides whether the the IFD entry in p is "interesting" and
+// stows away the data in the decoder.
+func (d *decoder) parseIFD(p []byte) error {
+ tag := d.byteOrder.Uint16(p[0:2])
+ switch tag {
+ case tBitsPerSample,
+ tExtraSamples,
+ tPhotometricInterpretation,
+ tCompression,
+ tPredictor,
+ tStripOffsets,
+ tStripByteCounts,
+ tRowsPerStrip,
+ tTileWidth,
+ tTileLength,
+ tTileOffsets,
+ tTileByteCounts,
+ tImageLength,
+ tImageWidth:
+ val, err := d.ifdUint(p)
+ if err != nil {
+ return err
+ }
+ d.features[int(tag)] = val
+ case tColorMap:
+ val, err := d.ifdUint(p)
+ if err != nil {
+ return err
+ }
+ numcolors := len(val) / 3
+ if len(val)%3 != 0 || numcolors <= 0 || numcolors > 256 {
+ return FormatError("bad ColorMap length")
+ }
+ d.palette = make([]color.Color, numcolors)
+ for i := 0; i < numcolors; i++ {
+ d.palette[i] = color.RGBA64{
+ uint16(val[i]),
+ uint16(val[i+numcolors]),
+ uint16(val[i+2*numcolors]),
+ 0xffff,
+ }
+ }
+ case tSampleFormat:
+ // Page 27 of the spec: If the SampleFormat is present and
+ // the value is not 1 [= unsigned integer data], a Baseline
+ // TIFF reader that cannot handle the SampleFormat value
+ // must terminate the import process gracefully.
+ val, err := d.ifdUint(p)
+ if err != nil {
+ return err
+ }
+ for _, v := range val {
+ if v != 1 {
+ return UnsupportedError("sample format")
+ }
+ }
+ }
+ return nil
+}
+
+// readBits reads n bits from the internal buffer starting at the current offset.
+func (d *decoder) readBits(n uint) (v uint32, ok bool) {
+ for d.nbits < n {
+ d.v <<= 8
+ if d.off >= len(d.buf) {
+ return 0, false
+ }
+ d.v |= uint32(d.buf[d.off])
+ d.off++
+ d.nbits += 8
+ }
+ d.nbits -= n
+ rv := d.v >> d.nbits
+ d.v &^= rv << d.nbits
+ return rv, true
+}
+
+// flushBits discards the unread bits in the buffer used by readBits.
+// It is used at the end of a line.
+func (d *decoder) flushBits() {
+ d.v = 0
+ d.nbits = 0
+}
+
+// minInt returns the smaller of x or y.
+func minInt(a, b int) int {
+ if a <= b {
+ return a
+ }
+ return b
+}
+
+// decode decodes the raw data of an image.
+// It reads from d.buf and writes the strip or tile into dst.
+func (d *decoder) decode(dst image.Image, xmin, ymin, xmax, ymax int) error {
+ d.off = 0
+
+ // Apply horizontal predictor if necessary.
+ // In this case, p contains the color difference to the preceding pixel.
+ // See page 64-65 of the spec.
+ if d.firstVal(tPredictor) == prHorizontal {
+ switch d.bpp {
+ case 16:
+ var off int
+ n := 2 * len(d.features[tBitsPerSample]) // bytes per sample times samples per pixel
+ for y := ymin; y < ymax; y++ {
+ off += n
+ for x := 0; x < (xmax-xmin-1)*n; x += 2 {
+ if off+2 > len(d.buf) {
+ return errNoPixels
+ }
+ v0 := d.byteOrder.Uint16(d.buf[off-n : off-n+2])
+ v1 := d.byteOrder.Uint16(d.buf[off : off+2])
+ d.byteOrder.PutUint16(d.buf[off:off+2], v1+v0)
+ off += 2
+ }
+ }
+ case 8:
+ var off int
+ n := 1 * len(d.features[tBitsPerSample]) // bytes per sample times samples per pixel
+ for y := ymin; y < ymax; y++ {
+ off += n
+ for x := 0; x < (xmax-xmin-1)*n; x++ {
+ if off >= len(d.buf) {
+ return errNoPixels
+ }
+ d.buf[off] += d.buf[off-n]
+ off++
+ }
+ }
+ case 1:
+ return UnsupportedError("horizontal predictor with 1 BitsPerSample")
+ }
+ }
+
+ rMaxX := minInt(xmax, dst.Bounds().Max.X)
+ rMaxY := minInt(ymax, dst.Bounds().Max.Y)
+ switch d.mode {
+ case mGray, mGrayInvert:
+ if d.bpp == 16 {
+ img := dst.(*image.Gray16)
+ for y := ymin; y < rMaxY; y++ {
+ for x := xmin; x < rMaxX; x++ {
+ if d.off+2 > len(d.buf) {
+ return errNoPixels
+ }
+ v := d.byteOrder.Uint16(d.buf[d.off : d.off+2])
+ d.off += 2
+ if d.mode == mGrayInvert {
+ v = 0xffff - v
+ }
+ img.SetGray16(x, y, color.Gray16{v})
+ }
+ }
+ } else {
+ img := dst.(*image.Gray)
+ max := uint32((1 << d.bpp) - 1)
+ for y := ymin; y < rMaxY; y++ {
+ for x := xmin; x < rMaxX; x++ {
+ v, ok := d.readBits(d.bpp)
+ if !ok {
+ return errNoPixels
+ }
+ v = v * 0xff / max
+ if d.mode == mGrayInvert {
+ v = 0xff - v
+ }
+ img.SetGray(x, y, color.Gray{uint8(v)})
+ }
+ d.flushBits()
+ }
+ }
+ case mPaletted:
+ img := dst.(*image.Paletted)
+ for y := ymin; y < rMaxY; y++ {
+ for x := xmin; x < rMaxX; x++ {
+ v, ok := d.readBits(d.bpp)
+ if !ok {
+ return errNoPixels
+ }
+ img.SetColorIndex(x, y, uint8(v))
+ }
+ d.flushBits()
+ }
+ case mRGB:
+ if d.bpp == 16 {
+ img := dst.(*image.RGBA64)
+ for y := ymin; y < rMaxY; y++ {
+ for x := xmin; x < rMaxX; x++ {
+ if d.off+6 > len(d.buf) {
+ return errNoPixels
+ }
+ r := d.byteOrder.Uint16(d.buf[d.off+0 : d.off+2])
+ g := d.byteOrder.Uint16(d.buf[d.off+2 : d.off+4])
+ b := d.byteOrder.Uint16(d.buf[d.off+4 : d.off+6])
+ d.off += 6
+ img.SetRGBA64(x, y, color.RGBA64{r, g, b, 0xffff})
+ }
+ }
+ } else {
+ img := dst.(*image.RGBA)
+ for y := ymin; y < rMaxY; y++ {
+ min := img.PixOffset(xmin, y)
+ max := img.PixOffset(rMaxX, y)
+ off := (y - ymin) * (xmax - xmin) * 3
+ for i := min; i < max; i += 4 {
+ if off+3 > len(d.buf) {
+ return errNoPixels
+ }
+ img.Pix[i+0] = d.buf[off+0]
+ img.Pix[i+1] = d.buf[off+1]
+ img.Pix[i+2] = d.buf[off+2]
+ img.Pix[i+3] = 0xff
+ off += 3
+ }
+ }
+ }
+ case mNRGBA:
+ if d.bpp == 16 {
+ img := dst.(*image.NRGBA64)
+ for y := ymin; y < rMaxY; y++ {
+ for x := xmin; x < rMaxX; x++ {
+ if d.off+8 > len(d.buf) {
+ return errNoPixels
+ }
+ r := d.byteOrder.Uint16(d.buf[d.off+0 : d.off+2])
+ g := d.byteOrder.Uint16(d.buf[d.off+2 : d.off+4])
+ b := d.byteOrder.Uint16(d.buf[d.off+4 : d.off+6])
+ a := d.byteOrder.Uint16(d.buf[d.off+6 : d.off+8])
+ d.off += 8
+ img.SetNRGBA64(x, y, color.NRGBA64{r, g, b, a})
+ }
+ }
+ } else {
+ img := dst.(*image.NRGBA)
+ for y := ymin; y < rMaxY; y++ {
+ min := img.PixOffset(xmin, y)
+ max := img.PixOffset(rMaxX, y)
+ i0, i1 := (y-ymin)*(xmax-xmin)*4, (y-ymin+1)*(xmax-xmin)*4
+ if i1 > len(d.buf) {
+ return errNoPixels
+ }
+ copy(img.Pix[min:max], d.buf[i0:i1])
+ }
+ }
+ case mRGBA:
+ if d.bpp == 16 {
+ img := dst.(*image.RGBA64)
+ for y := ymin; y < rMaxY; y++ {
+ for x := xmin; x < rMaxX; x++ {
+ if d.off+8 > len(d.buf) {
+ return errNoPixels
+ }
+ r := d.byteOrder.Uint16(d.buf[d.off+0 : d.off+2])
+ g := d.byteOrder.Uint16(d.buf[d.off+2 : d.off+4])
+ b := d.byteOrder.Uint16(d.buf[d.off+4 : d.off+6])
+ a := d.byteOrder.Uint16(d.buf[d.off+6 : d.off+8])
+ d.off += 8
+ img.SetRGBA64(x, y, color.RGBA64{r, g, b, a})
+ }
+ }
+ } else {
+ img := dst.(*image.RGBA)
+ for y := ymin; y < rMaxY; y++ {
+ min := img.PixOffset(xmin, y)
+ max := img.PixOffset(rMaxX, y)
+ i0, i1 := (y-ymin)*(xmax-xmin)*4, (y-ymin+1)*(xmax-xmin)*4
+ if i1 > len(d.buf) {
+ return errNoPixels
+ }
+ copy(img.Pix[min:max], d.buf[i0:i1])
+ }
+ }
+ }
+
+ return nil
+}
+
+func newDecoder(r io.Reader) (*decoder, error) {
+ d := &decoder{
+ r: newReaderAt(r),
+ features: make(map[int][]uint),
+ }
+
+ p := make([]byte, 8)
+ if _, err := d.r.ReadAt(p, 0); err != nil {
+ return nil, err
+ }
+ switch string(p[0:4]) {
+ case leHeader:
+ d.byteOrder = binary.LittleEndian
+ case beHeader:
+ d.byteOrder = binary.BigEndian
+ default:
+ return nil, FormatError("malformed header")
+ }
+
+ ifdOffset := int64(d.byteOrder.Uint32(p[4:8]))
+
+ // The first two bytes contain the number of entries (12 bytes each).
+ if _, err := d.r.ReadAt(p[0:2], ifdOffset); err != nil {
+ return nil, err
+ }
+ numItems := int(d.byteOrder.Uint16(p[0:2]))
+
+ // All IFD entries are read in one chunk.
+ p = make([]byte, ifdLen*numItems)
+ if _, err := d.r.ReadAt(p, ifdOffset+2); err != nil {
+ return nil, err
+ }
+
+ for i := 0; i < len(p); i += ifdLen {
+ if err := d.parseIFD(p[i : i+ifdLen]); err != nil {
+ return nil, err
+ }
+ }
+
+ d.config.Width = int(d.firstVal(tImageWidth))
+ d.config.Height = int(d.firstVal(tImageLength))
+
+ if _, ok := d.features[tBitsPerSample]; !ok {
+ return nil, FormatError("BitsPerSample tag missing")
+ }
+ d.bpp = d.firstVal(tBitsPerSample)
+ switch d.bpp {
+ case 0:
+ return nil, FormatError("BitsPerSample must not be 0")
+ case 1, 8, 16:
+ // Nothing to do, these are accepted by this implementation.
+ default:
+ return nil, UnsupportedError(fmt.Sprintf("BitsPerSample of %v", d.bpp))
+ }
+
+ // Determine the image mode.
+ switch d.firstVal(tPhotometricInterpretation) {
+ case pRGB:
+ if d.bpp == 16 {
+ for _, b := range d.features[tBitsPerSample] {
+ if b != 16 {
+ return nil, FormatError("wrong number of samples for 16bit RGB")
+ }
+ }
+ } else {
+ for _, b := range d.features[tBitsPerSample] {
+ if b != 8 {
+ return nil, FormatError("wrong number of samples for 8bit RGB")
+ }
+ }
+ }
+ // RGB images normally have 3 samples per pixel.
+ // If there are more, ExtraSamples (p. 31-32 of the spec)
+ // gives their meaning (usually an alpha channel).
+ //
+ // This implementation does not support extra samples
+ // of an unspecified type.
+ switch len(d.features[tBitsPerSample]) {
+ case 3:
+ d.mode = mRGB
+ if d.bpp == 16 {
+ d.config.ColorModel = color.RGBA64Model
+ } else {
+ d.config.ColorModel = color.RGBAModel
+ }
+ case 4:
+ switch d.firstVal(tExtraSamples) {
+ case 1:
+ d.mode = mRGBA
+ if d.bpp == 16 {
+ d.config.ColorModel = color.RGBA64Model
+ } else {
+ d.config.ColorModel = color.RGBAModel
+ }
+ case 2:
+ d.mode = mNRGBA
+ if d.bpp == 16 {
+ d.config.ColorModel = color.NRGBA64Model
+ } else {
+ d.config.ColorModel = color.NRGBAModel
+ }
+ default:
+ return nil, FormatError("wrong number of samples for RGB")
+ }
+ default:
+ return nil, FormatError("wrong number of samples for RGB")
+ }
+ case pPaletted:
+ d.mode = mPaletted
+ d.config.ColorModel = color.Palette(d.palette)
+ case pWhiteIsZero:
+ d.mode = mGrayInvert
+ if d.bpp == 16 {
+ d.config.ColorModel = color.Gray16Model
+ } else {
+ d.config.ColorModel = color.GrayModel
+ }
+ case pBlackIsZero:
+ d.mode = mGray
+ if d.bpp == 16 {
+ d.config.ColorModel = color.Gray16Model
+ } else {
+ d.config.ColorModel = color.GrayModel
+ }
+ default:
+ return nil, UnsupportedError("color model")
+ }
+
+ return d, nil
+}
+
+// DecodeConfig returns the color model and dimensions of a TIFF image without
+// decoding the entire image.
+func DecodeConfig(r io.Reader) (image.Config, error) {
+ d, err := newDecoder(r)
+ if err != nil {
+ return image.Config{}, err
+ }
+ return d.config, nil
+}
+
+// Decode reads a TIFF image from r and returns it as an image.Image.
+// The type of Image returned depends on the contents of the TIFF.
+func Decode(r io.Reader) (img image.Image, err error) {
+ d, err := newDecoder(r)
+ if err != nil {
+ return
+ }
+
+ blockPadding := false
+ blockWidth := d.config.Width
+ blockHeight := d.config.Height
+ blocksAcross := 1
+ blocksDown := 1
+
+ if d.config.Width == 0 {
+ blocksAcross = 0
+ }
+ if d.config.Height == 0 {
+ blocksDown = 0
+ }
+
+ var blockOffsets, blockCounts []uint
+
+ if int(d.firstVal(tTileWidth)) != 0 {
+ blockPadding = true
+
+ blockWidth = int(d.firstVal(tTileWidth))
+ blockHeight = int(d.firstVal(tTileLength))
+
+ if blockWidth != 0 {
+ blocksAcross = (d.config.Width + blockWidth - 1) / blockWidth
+ }
+ if blockHeight != 0 {
+ blocksDown = (d.config.Height + blockHeight - 1) / blockHeight
+ }
+
+ blockCounts = d.features[tTileByteCounts]
+ blockOffsets = d.features[tTileOffsets]
+
+ } else {
+ if int(d.firstVal(tRowsPerStrip)) != 0 {
+ blockHeight = int(d.firstVal(tRowsPerStrip))
+ }
+
+ if blockHeight != 0 {
+ blocksDown = (d.config.Height + blockHeight - 1) / blockHeight
+ }
+
+ blockOffsets = d.features[tStripOffsets]
+ blockCounts = d.features[tStripByteCounts]
+ }
+
+ // Check if we have the right number of strips/tiles, offsets and counts.
+ if n := blocksAcross * blocksDown; len(blockOffsets) < n || len(blockCounts) < n {
+ return nil, FormatError("inconsistent header")
+ }
+
+ imgRect := image.Rect(0, 0, d.config.Width, d.config.Height)
+ switch d.mode {
+ case mGray, mGrayInvert:
+ if d.bpp == 16 {
+ img = image.NewGray16(imgRect)
+ } else {
+ img = image.NewGray(imgRect)
+ }
+ case mPaletted:
+ img = image.NewPaletted(imgRect, d.palette)
+ case mNRGBA:
+ if d.bpp == 16 {
+ img = image.NewNRGBA64(imgRect)
+ } else {
+ img = image.NewNRGBA(imgRect)
+ }
+ case mRGB, mRGBA:
+ if d.bpp == 16 {
+ img = image.NewRGBA64(imgRect)
+ } else {
+ img = image.NewRGBA(imgRect)
+ }
+ }
+
+ for i := 0; i < blocksAcross; i++ {
+ blkW := blockWidth
+ if !blockPadding && i == blocksAcross-1 && d.config.Width%blockWidth != 0 {
+ blkW = d.config.Width % blockWidth
+ }
+ for j := 0; j < blocksDown; j++ {
+ blkH := blockHeight
+ if !blockPadding && j == blocksDown-1 && d.config.Height%blockHeight != 0 {
+ blkH = d.config.Height % blockHeight
+ }
+ offset := int64(blockOffsets[j*blocksAcross+i])
+ n := int64(blockCounts[j*blocksAcross+i])
+ switch d.firstVal(tCompression) {
+
+ // According to the spec, Compression does not have a default value,
+ // but some tools interpret a missing Compression value as none so we do
+ // the same.
+ case cNone, 0:
+ if b, ok := d.r.(*buffer); ok {
+ d.buf, err = b.Slice(int(offset), int(n))
+ } else {
+ d.buf = make([]byte, n)
+ _, err = d.r.ReadAt(d.buf, offset)
+ }
+ case cLZW:
+ r := lzw.NewReader(io.NewSectionReader(d.r, offset, n), lzw.MSB, 8)
+ d.buf, err = ioutil.ReadAll(r)
+ r.Close()
+ case cDeflate, cDeflateOld:
+ var r io.ReadCloser
+ r, err = zlib.NewReader(io.NewSectionReader(d.r, offset, n))
+ if err != nil {
+ return nil, err
+ }
+ d.buf, err = ioutil.ReadAll(r)
+ r.Close()
+ case cPackBits:
+ d.buf, err = unpackBits(io.NewSectionReader(d.r, offset, n))
+ default:
+ err = UnsupportedError(fmt.Sprintf("compression value %d", d.firstVal(tCompression)))
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ xmin := i * blockWidth
+ ymin := j * blockHeight
+ xmax := xmin + blkW
+ ymax := ymin + blkH
+ err = d.decode(img, xmin, ymin, xmax, ymax)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+ return
+}
+
+func init() {
+ image.RegisterFormat("tiff", leHeader, Decode, DecodeConfig)
+ image.RegisterFormat("tiff", beHeader, Decode, DecodeConfig)
+}
diff --git a/Godeps/_workspace/src/golang.org/x/image/tiff/writer.go b/Godeps/_workspace/src/golang.org/x/image/tiff/writer.go
new file mode 100644
index 000000000..c8a01cea7
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/image/tiff/writer.go
@@ -0,0 +1,438 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tiff
+
+import (
+ "bytes"
+ "compress/zlib"
+ "encoding/binary"
+ "image"
+ "io"
+ "sort"
+)
+
+// The TIFF format allows to choose the order of the different elements freely.
+// The basic structure of a TIFF file written by this package is:
+//
+// 1. Header (8 bytes).
+// 2. Image data.
+// 3. Image File Directory (IFD).
+// 4. "Pointer area" for larger entries in the IFD.
+
+// We only write little-endian TIFF files.
+var enc = binary.LittleEndian
+
+// An ifdEntry is a single entry in an Image File Directory.
+// A value of type dtRational is composed of two 32-bit values,
+// thus data contains two uints (numerator and denominator) for a single number.
+type ifdEntry struct {
+ tag int
+ datatype int
+ data []uint32
+}
+
+func (e ifdEntry) putData(p []byte) {
+ for _, d := range e.data {
+ switch e.datatype {
+ case dtByte, dtASCII:
+ p[0] = byte(d)
+ p = p[1:]
+ case dtShort:
+ enc.PutUint16(p, uint16(d))
+ p = p[2:]
+ case dtLong, dtRational:
+ enc.PutUint32(p, uint32(d))
+ p = p[4:]
+ }
+ }
+}
+
+type byTag []ifdEntry
+
+func (d byTag) Len() int { return len(d) }
+func (d byTag) Less(i, j int) bool { return d[i].tag < d[j].tag }
+func (d byTag) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
+
+func encodeGray(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error {
+ if !predictor {
+ return writePix(w, pix, dy, dx, stride)
+ }
+ buf := make([]byte, dx)
+ for y := 0; y < dy; y++ {
+ min := y*stride + 0
+ max := y*stride + dx
+ off := 0
+ var v0 uint8
+ for i := min; i < max; i++ {
+ v1 := pix[i]
+ buf[off] = v1 - v0
+ v0 = v1
+ off++
+ }
+ if _, err := w.Write(buf); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func encodeGray16(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error {
+ buf := make([]byte, dx*2)
+ for y := 0; y < dy; y++ {
+ min := y*stride + 0
+ max := y*stride + dx*2
+ off := 0
+ var v0 uint16
+ for i := min; i < max; i += 2 {
+ // An image.Gray16's Pix is in big-endian order.
+ v1 := uint16(pix[i])<<8 | uint16(pix[i+1])
+ if predictor {
+ v0, v1 = v1, v1-v0
+ }
+ // We only write little-endian TIFF files.
+ buf[off+0] = byte(v1)
+ buf[off+1] = byte(v1 >> 8)
+ off += 2
+ }
+ if _, err := w.Write(buf); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func encodeRGBA(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error {
+ if !predictor {
+ return writePix(w, pix, dy, dx*4, stride)
+ }
+ buf := make([]byte, dx*4)
+ for y := 0; y < dy; y++ {
+ min := y*stride + 0
+ max := y*stride + dx*4
+ off := 0
+ var r0, g0, b0, a0 uint8
+ for i := min; i < max; i += 4 {
+ r1, g1, b1, a1 := pix[i+0], pix[i+1], pix[i+2], pix[i+3]
+ buf[off+0] = r1 - r0
+ buf[off+1] = g1 - g0
+ buf[off+2] = b1 - b0
+ buf[off+3] = a1 - a0
+ off += 4
+ r0, g0, b0, a0 = r1, g1, b1, a1
+ }
+ if _, err := w.Write(buf); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func encodeRGBA64(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error {
+ buf := make([]byte, dx*8)
+ for y := 0; y < dy; y++ {
+ min := y*stride + 0
+ max := y*stride + dx*8
+ off := 0
+ var r0, g0, b0, a0 uint16
+ for i := min; i < max; i += 8 {
+ // An image.RGBA64's Pix is in big-endian order.
+ r1 := uint16(pix[i+0])<<8 | uint16(pix[i+1])
+ g1 := uint16(pix[i+2])<<8 | uint16(pix[i+3])
+ b1 := uint16(pix[i+4])<<8 | uint16(pix[i+5])
+ a1 := uint16(pix[i+6])<<8 | uint16(pix[i+7])
+ if predictor {
+ r0, r1 = r1, r1-r0
+ g0, g1 = g1, g1-g0
+ b0, b1 = b1, b1-b0
+ a0, a1 = a1, a1-a0
+ }
+ // We only write little-endian TIFF files.
+ buf[off+0] = byte(r1)
+ buf[off+1] = byte(r1 >> 8)
+ buf[off+2] = byte(g1)
+ buf[off+3] = byte(g1 >> 8)
+ buf[off+4] = byte(b1)
+ buf[off+5] = byte(b1 >> 8)
+ buf[off+6] = byte(a1)
+ buf[off+7] = byte(a1 >> 8)
+ off += 8
+ }
+ if _, err := w.Write(buf); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func encode(w io.Writer, m image.Image, predictor bool) error {
+ bounds := m.Bounds()
+ buf := make([]byte, 4*bounds.Dx())
+ for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
+ off := 0
+ if predictor {
+ var r0, g0, b0, a0 uint8
+ for x := bounds.Min.X; x < bounds.Max.X; x++ {
+ r, g, b, a := m.At(x, y).RGBA()
+ r1 := uint8(r >> 8)
+ g1 := uint8(g >> 8)
+ b1 := uint8(b >> 8)
+ a1 := uint8(a >> 8)
+ buf[off+0] = r1 - r0
+ buf[off+1] = g1 - g0
+ buf[off+2] = b1 - b0
+ buf[off+3] = a1 - a0
+ off += 4
+ r0, g0, b0, a0 = r1, g1, b1, a1
+ }
+ } else {
+ for x := bounds.Min.X; x < bounds.Max.X; x++ {
+ r, g, b, a := m.At(x, y).RGBA()
+ buf[off+0] = uint8(r >> 8)
+ buf[off+1] = uint8(g >> 8)
+ buf[off+2] = uint8(b >> 8)
+ buf[off+3] = uint8(a >> 8)
+ off += 4
+ }
+ }
+ if _, err := w.Write(buf); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// writePix writes the internal byte array of an image to w. It is less general
+// but much faster then encode. writePix is used when pix directly
+// corresponds to one of the TIFF image types.
+func writePix(w io.Writer, pix []byte, nrows, length, stride int) error {
+ if length == stride {
+ _, err := w.Write(pix[:nrows*length])
+ return err
+ }
+ for ; nrows > 0; nrows-- {
+ if _, err := w.Write(pix[:length]); err != nil {
+ return err
+ }
+ pix = pix[stride:]
+ }
+ return nil
+}
+
+func writeIFD(w io.Writer, ifdOffset int, d []ifdEntry) error {
+ var buf [ifdLen]byte
+ // Make space for "pointer area" containing IFD entry data
+ // longer than 4 bytes.
+ parea := make([]byte, 1024)
+ pstart := ifdOffset + ifdLen*len(d) + 6
+ var o int // Current offset in parea.
+
+ // The IFD has to be written with the tags in ascending order.
+ sort.Sort(byTag(d))
+
+ // Write the number of entries in this IFD.
+ if err := binary.Write(w, enc, uint16(len(d))); err != nil {
+ return err
+ }
+ for _, ent := range d {
+ enc.PutUint16(buf[0:2], uint16(ent.tag))
+ enc.PutUint16(buf[2:4], uint16(ent.datatype))
+ count := uint32(len(ent.data))
+ if ent.datatype == dtRational {
+ count /= 2
+ }
+ enc.PutUint32(buf[4:8], count)
+ datalen := int(count * lengths[ent.datatype])
+ if datalen <= 4 {
+ ent.putData(buf[8:12])
+ } else {
+ if (o + datalen) > len(parea) {
+ newlen := len(parea) + 1024
+ for (o + datalen) > newlen {
+ newlen += 1024
+ }
+ newarea := make([]byte, newlen)
+ copy(newarea, parea)
+ parea = newarea
+ }
+ ent.putData(parea[o : o+datalen])
+ enc.PutUint32(buf[8:12], uint32(pstart+o))
+ o += datalen
+ }
+ if _, err := w.Write(buf[:]); err != nil {
+ return err
+ }
+ }
+ // The IFD ends with the offset of the next IFD in the file,
+ // or zero if it is the last one (page 14).
+ if err := binary.Write(w, enc, uint32(0)); err != nil {
+ return err
+ }
+ _, err := w.Write(parea[:o])
+ return err
+}
+
+// Options are the encoding parameters.
+type Options struct {
+ // Compression is the type of compression used.
+ Compression CompressionType
+ // Predictor determines whether a differencing predictor is used;
+ // if true, instead of each pixel's color, the color difference to the
+ // preceding one is saved. This improves the compression for certain
+ // types of images and compressors. For example, it works well for
+ // photos with Deflate compression.
+ Predictor bool
+}
+
+// Encode writes the image m to w. opt determines the options used for
+// encoding, such as the compression type. If opt is nil, an uncompressed
+// image is written.
+func Encode(w io.Writer, m image.Image, opt *Options) error {
+ d := m.Bounds().Size()
+
+ compression := uint32(cNone)
+ predictor := false
+ if opt != nil {
+ compression = opt.Compression.specValue()
+ // The predictor field is only used with LZW. See page 64 of the spec.
+ predictor = opt.Predictor && compression == cLZW
+ }
+
+ _, err := io.WriteString(w, leHeader)
+ if err != nil {
+ return err
+ }
+
+ // Compressed data is written into a buffer first, so that we
+ // know the compressed size.
+ var buf bytes.Buffer
+ // dst holds the destination for the pixel data of the image --
+ // either w or a writer to buf.
+ var dst io.Writer
+ // imageLen is the length of the pixel data in bytes.
+ // The offset of the IFD is imageLen + 8 header bytes.
+ var imageLen int
+
+ switch compression {
+ case cNone:
+ dst = w
+ // Write IFD offset before outputting pixel data.
+ switch m.(type) {
+ case *image.Paletted:
+ imageLen = d.X * d.Y * 1
+ case *image.Gray:
+ imageLen = d.X * d.Y * 1
+ case *image.Gray16:
+ imageLen = d.X * d.Y * 2
+ case *image.RGBA64:
+ imageLen = d.X * d.Y * 8
+ case *image.NRGBA64:
+ imageLen = d.X * d.Y * 8
+ default:
+ imageLen = d.X * d.Y * 4
+ }
+ err = binary.Write(w, enc, uint32(imageLen+8))
+ if err != nil {
+ return err
+ }
+ case cDeflate:
+ dst = zlib.NewWriter(&buf)
+ }
+
+ pr := uint32(prNone)
+ photometricInterpretation := uint32(pRGB)
+ samplesPerPixel := uint32(4)
+ bitsPerSample := []uint32{8, 8, 8, 8}
+ extraSamples := uint32(0)
+ colorMap := []uint32{}
+
+ if predictor {
+ pr = prHorizontal
+ }
+ switch m := m.(type) {
+ case *image.Paletted:
+ photometricInterpretation = pPaletted
+ samplesPerPixel = 1
+ bitsPerSample = []uint32{8}
+ colorMap = make([]uint32, 256*3)
+ for i := 0; i < 256 && i < len(m.Palette); i++ {
+ r, g, b, _ := m.Palette[i].RGBA()
+ colorMap[i+0*256] = uint32(r)
+ colorMap[i+1*256] = uint32(g)
+ colorMap[i+2*256] = uint32(b)
+ }
+ err = encodeGray(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.Gray:
+ photometricInterpretation = pBlackIsZero
+ samplesPerPixel = 1
+ bitsPerSample = []uint32{8}
+ err = encodeGray(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.Gray16:
+ photometricInterpretation = pBlackIsZero
+ samplesPerPixel = 1
+ bitsPerSample = []uint32{16}
+ err = encodeGray16(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.NRGBA:
+ extraSamples = 2 // Unassociated alpha.
+ err = encodeRGBA(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.NRGBA64:
+ extraSamples = 2 // Unassociated alpha.
+ bitsPerSample = []uint32{16, 16, 16, 16}
+ err = encodeRGBA64(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.RGBA:
+ extraSamples = 1 // Associated alpha.
+ err = encodeRGBA(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.RGBA64:
+ extraSamples = 1 // Associated alpha.
+ bitsPerSample = []uint32{16, 16, 16, 16}
+ err = encodeRGBA64(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ default:
+ extraSamples = 1 // Associated alpha.
+ err = encode(dst, m, predictor)
+ }
+ if err != nil {
+ return err
+ }
+
+ if compression != cNone {
+ if err = dst.(io.Closer).Close(); err != nil {
+ return err
+ }
+ imageLen = buf.Len()
+ if err = binary.Write(w, enc, uint32(imageLen+8)); err != nil {
+ return err
+ }
+ if _, err = buf.WriteTo(w); err != nil {
+ return err
+ }
+ }
+
+ ifd := []ifdEntry{
+ {tImageWidth, dtShort, []uint32{uint32(d.X)}},
+ {tImageLength, dtShort, []uint32{uint32(d.Y)}},
+ {tBitsPerSample, dtShort, bitsPerSample},
+ {tCompression, dtShort, []uint32{compression}},
+ {tPhotometricInterpretation, dtShort, []uint32{photometricInterpretation}},
+ {tStripOffsets, dtLong, []uint32{8}},
+ {tSamplesPerPixel, dtShort, []uint32{samplesPerPixel}},
+ {tRowsPerStrip, dtShort, []uint32{uint32(d.Y)}},
+ {tStripByteCounts, dtLong, []uint32{uint32(imageLen)}},
+ // There is currently no support for storing the image
+ // resolution, so give a bogus value of 72x72 dpi.
+ {tXResolution, dtRational, []uint32{72, 1}},
+ {tYResolution, dtRational, []uint32{72, 1}},
+ {tResolutionUnit, dtShort, []uint32{resPerInch}},
+ }
+ if pr != prNone {
+ ifd = append(ifd, ifdEntry{tPredictor, dtShort, []uint32{pr}})
+ }
+ if len(colorMap) != 0 {
+ ifd = append(ifd, ifdEntry{tColorMap, dtShort, colorMap})
+ }
+ if extraSamples > 0 {
+ ifd = append(ifd, ifdEntry{tExtraSamples, dtShort, []uint32{extraSamples}})
+ }
+
+ return writeIFD(w, imageLen+8, ifd)
+}
diff --git a/Makefile b/Makefile
index 5cb5467f6..8d6d30735 100644
--- a/Makefile
+++ b/Makefile
@@ -106,7 +106,12 @@ travis:
@sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak
+ mv doc/README.md doc/index.md
+ mkdocs build
+ cp -r documentation-html $(DIST_PATH)/documentation-html
+
tar -C dist -czf $(DIST_PATH).tar.gz mattermost
+ rm -r $(DIST_PATH)
build:
@$(GO) build $(GOFLAGS) ./...
@@ -140,11 +145,11 @@ check: install
test: install
@mkdir -p logs
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./api || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s ./model || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./store || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./utils || exit 1
+ @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./web || exit 1
benchmark: install
@mkdir -p logs
diff --git a/api/admin.go b/api/admin.go
index 568d8f6e8..d9714d6d2 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -23,8 +23,10 @@ func InitAdmin(r *mux.Router) {
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
- sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
+ sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
+ sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
+
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -59,6 +61,26 @@ func getClientProperties(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(utils.ClientProperties)))
}
+func logClient(c *Context, w http.ResponseWriter, r *http.Request) {
+ m := model.MapFromJson(r.Body)
+
+ lvl := m["level"]
+ msg := m["message"]
+
+ if len(msg) > 400 {
+ msg = msg[0:399]
+ }
+
+ if lvl == "ERROR" {
+ err := model.NewAppError("client", msg, "")
+ c.LogError(err)
+ }
+
+ rm := make(map[string]string)
+ rm["SUCCESS"] = "true"
+ w.Write([]byte(model.MapToJson(rm)))
+}
+
func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("getConfig") {
return
@@ -82,18 +104,11 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if len(cfg.ServiceSettings.ListenAddress) == 0 {
- c.SetInvalidParam("saveConfig", "config")
- return
- }
-
- if cfg.TeamSettings.MaxUsersPerTeam == 0 {
- c.SetInvalidParam("saveConfig", "config")
+ if err := cfg.IsValid(); err != nil {
+ c.Err = err
return
}
- // TODO run some cleanup validators
-
utils.SaveConfig(utils.CfgFileName, cfg)
utils.LoadConfig(utils.CfgFileName)
json := utils.Cfg.ToJson()
diff --git a/api/channel.go b/api/channel.go
index 896e22793..5e13fa18a 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -23,7 +23,7 @@ func InitChannel(r *mux.Router) {
sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST")
sr.Handle("/update_desc", ApiUserRequired(updateChannelDesc)).Methods("POST")
- sr.Handle("/update_notify_level", ApiUserRequired(updateNotifyLevel)).Methods("POST")
+ sr.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST")
sr.Handle("/{id:[A-Za-z0-9]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/join", ApiUserRequired(joinChannel)).Methods("POST")
@@ -76,7 +76,7 @@ func CreateChannel(c *Context, channel *model.Channel, addMember bool) (*model.C
if addMember {
cm := &model.ChannelMember{ChannelId: sc.Id, UserId: c.Session.UserId,
- Roles: model.CHANNEL_ROLE_ADMIN, NotifyLevel: model.CHANNEL_NOTIFY_ALL}
+ Roles: model.CHANNEL_ROLE_ADMIN, NotifyProps: model.GetDefaultChannelNotifyProps()}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
return nil, cmresult.Err
@@ -134,8 +134,7 @@ func CreateDirectChannel(c *Context, otherUserId string) (*model.Channel, *model
if sc, err := CreateChannel(c, channel, true); err != nil {
return nil, err
} else {
- cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId,
- Roles: "", NotifyLevel: model.CHANNEL_NOTIFY_ALL}
+ cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId, Roles: "", NotifyProps: model.GetDefaultChannelNotifyProps()}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
return nil, cmresult.Err
@@ -282,7 +281,7 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) {
// lets make sure the user is valid
if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = result.Err
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId)
return
}
@@ -372,7 +371,8 @@ func JoinChannel(c *Context, channelId string, role string) {
}
if channel.Type == model.CHANNEL_OPEN {
- cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: role}
+ cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId,
+ Roles: role, NotifyProps: model.GetDefaultChannelNotifyProps()}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
c.Err = cmresult.Err
@@ -405,7 +405,9 @@ func JoinDefaultChannels(user *model.User, channelRole string) *model.AppError {
if result := <-Srv.Store.Channel().GetByName(user.TeamId, "town-square"); result.Err != nil {
err = result.Err
} else {
- cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
+ cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id,
+ Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()}
+
if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
err = cmResult.Err
}
@@ -414,7 +416,9 @@ func JoinDefaultChannels(user *model.User, channelRole string) *model.AppError {
if result := <-Srv.Store.Channel().GetByName(user.TeamId, "off-topic"); result.Err != nil {
err = result.Err
} else {
- cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
+ cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id,
+ Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()}
+
if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
err = cmResult.Err
}
@@ -694,7 +698,7 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
oUser := oresult.Data.(*model.User)
- cm := &model.ChannelMember{ChannelId: channel.Id, UserId: userId, NotifyLevel: model.CHANNEL_NOTIFY_ALL}
+ cm := &model.ChannelMember{ChannelId: channel.Id, UserId: userId, NotifyProps: model.GetDefaultChannelNotifyProps()}
if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
l4g.Error("Failed to add member user_id=%v channel_id=%v err=%v", userId, id, cmresult.Err)
@@ -784,23 +788,18 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
}
-func updateNotifyLevel(c *Context, w http.ResponseWriter, r *http.Request) {
+func updateNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) {
data := model.MapFromJson(r.Body)
+
userId := data["user_id"]
if len(userId) != 26 {
- c.SetInvalidParam("updateNotifyLevel", "user_id")
+ c.SetInvalidParam("updateMarkUnreadLevel", "user_id")
return
}
channelId := data["channel_id"]
if len(channelId) != 26 {
- c.SetInvalidParam("updateNotifyLevel", "channel_id")
- return
- }
-
- notifyLevel := data["notify_level"]
- if len(notifyLevel) == 0 || !model.IsChannelNotifyLevelValid(notifyLevel) {
- c.SetInvalidParam("updateNotifyLevel", "notify_level")
+ c.SetInvalidParam("updateMarkUnreadLevel", "channel_id")
return
}
@@ -814,10 +813,29 @@ func updateNotifyLevel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Channel().UpdateNotifyLevel(channelId, userId, notifyLevel); result.Err != nil {
+ result := <-Srv.Store.Channel().GetMember(channelId, userId)
+ if result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+
+ member := result.Data.(model.ChannelMember)
+
+ // update whichever notify properties have been provided, but don't change the others
+ if markUnread, exists := data["mark_unread"]; exists {
+ member.NotifyProps["mark_unread"] = markUnread
+ }
+
+ if desktop, exists := data["desktop"]; exists {
+ member.NotifyProps["desktop"] = desktop
+ }
+
+ if result := <-Srv.Store.Channel().UpdateMember(&member); result.Err != nil {
c.Err = result.Err
return
+ } else {
+ // return the updated notify properties including any unchanged ones
+ w.Write([]byte(model.MapToJson(member.NotifyProps)))
}
- w.Write([]byte(model.MapToJson(data)))
}
diff --git a/api/channel_benchmark_test.go b/api/channel_benchmark_test.go
index 77e679c14..7820f4a03 100644
--- a/api/channel_benchmark_test.go
+++ b/api/channel_benchmark_test.go
@@ -255,7 +255,7 @@ func BenchmarkRemoveChannelMember(b *testing.B) {
}
}
-func BenchmarkUpdateNotifyLevel(b *testing.B) {
+func BenchmarkUpdateNotifyProps(b *testing.B) {
var (
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
)
@@ -271,9 +271,10 @@ func BenchmarkUpdateNotifyLevel(b *testing.B) {
for i := range data {
newmap := map[string]string{
- "channel_id": channels[i].Id,
- "user_id": user.Id,
- "notify_level": model.CHANNEL_NOTIFY_MENTION,
+ "channel_id": channels[i].Id,
+ "user_id": user.Id,
+ "desktop": model.CHANNEL_NOTIFY_MENTION,
+ "mark_unread": model.CHANNEL_MARK_UNREAD_MENTION,
}
data[i] = newmap
}
@@ -282,7 +283,7 @@ func BenchmarkUpdateNotifyLevel(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := range channels {
- Client.Must(Client.UpdateNotifyLevel(data[j]))
+ Client.Must(Client.UpdateNotifyProps(data[j]))
}
}
}
diff --git a/api/channel_test.go b/api/channel_test.go
index 7845ac499..e6c7ed80e 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -803,7 +803,7 @@ func TestRemoveChannelMember(t *testing.T) {
}
-func TestUpdateNotifyLevel(t *testing.T) {
+func TestUpdateNotifyProps(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
@@ -821,55 +821,94 @@ func TestUpdateNotifyLevel(t *testing.T) {
data := make(map[string]string)
data["channel_id"] = channel1.Id
data["user_id"] = user.Id
- data["notify_level"] = model.CHANNEL_NOTIFY_MENTION
+ data["desktop"] = model.CHANNEL_NOTIFY_MENTION
timeBeforeUpdate := model.GetMillis()
time.Sleep(100 * time.Millisecond)
- if _, err := Client.UpdateNotifyLevel(data); err != nil {
+ // test updating desktop
+ if result, err := Client.UpdateNotifyProps(data); err != nil {
t.Fatal(err)
+ } else if notifyProps := result.Data.(map[string]string); notifyProps["desktop"] != model.CHANNEL_NOTIFY_MENTION {
+ t.Fatal("NotifyProps[\"desktop\"] did not update properly")
+ } else if notifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_ALL {
+ t.Fatalf("NotifyProps[\"mark_unread\"] changed to %v", notifyProps["mark_unread"])
}
rget := Client.Must(Client.GetChannels(""))
rdata := rget.Data.(*model.ChannelList)
- if len(rdata.Members) == 0 || rdata.Members[channel1.Id].NotifyLevel != data["notify_level"] {
- t.Fatal("NotifyLevel did not update properly")
+ if len(rdata.Members) == 0 || rdata.Members[channel1.Id].NotifyProps["desktop"] != data["desktop"] {
+ t.Fatal("NotifyProps[\"desktop\"] did not update properly")
+ } else if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
+ t.Fatal("LastUpdateAt did not update")
}
- if rdata.Members[channel1.Id].LastUpdateAt <= timeBeforeUpdate {
- t.Fatal("LastUpdateAt did not update")
+ // test an empty update
+ delete(data, "desktop")
+
+ if result, err := Client.UpdateNotifyProps(data); err != nil {
+ t.Fatal(err)
+ } else if notifyProps := result.Data.(map[string]string); notifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_ALL {
+ t.Fatalf("NotifyProps[\"mark_unread\"] changed to %v", notifyProps["mark_unread"])
+ } else if notifyProps["desktop"] != model.CHANNEL_NOTIFY_MENTION {
+ t.Fatalf("NotifyProps[\"desktop\"] changed to %v", notifyProps["desktop"])
}
+ // test updating mark unread
+ data["mark_unread"] = model.CHANNEL_MARK_UNREAD_MENTION
+
+ if result, err := Client.UpdateNotifyProps(data); err != nil {
+ t.Fatal(err)
+ } else if notifyProps := result.Data.(map[string]string); notifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_MENTION {
+ t.Fatal("NotifyProps[\"mark_unread\"] did not update properly")
+ } else if notifyProps["desktop"] != model.CHANNEL_NOTIFY_MENTION {
+ t.Fatalf("NotifyProps[\"desktop\"] changed to %v", notifyProps["desktop"])
+ }
+
+ // test updating both
+ data["desktop"] = model.CHANNEL_NOTIFY_NONE
+ data["mark_unread"] = model.CHANNEL_MARK_UNREAD_MENTION
+
+ if result, err := Client.UpdateNotifyProps(data); err != nil {
+ t.Fatal(err)
+ } else if notifyProps := result.Data.(map[string]string); notifyProps["desktop"] != model.CHANNEL_NOTIFY_NONE {
+ t.Fatal("NotifyProps[\"desktop\"] did not update properly")
+ } else if notifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_MENTION {
+ t.Fatal("NotifyProps[\"mark_unread\"] did not update properly")
+ }
+
+ // test error cases
data["user_id"] = "junk"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - bad user id")
}
data["user_id"] = "12345678901234567890123456"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - bad user id")
}
data["user_id"] = user.Id
data["channel_id"] = "junk"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - bad channel id")
}
data["channel_id"] = "12345678901234567890123456"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - bad channel id")
}
- data["channel_id"] = channel1.Id
- data["notify_level"] = ""
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
- t.Fatal("Should have errored - empty notify level")
+ data["desktop"] = "junk"
+ data["mark_unread"] = model.CHANNEL_MARK_UNREAD_ALL
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
+ t.Fatal("Should have errored - bad desktop notify level")
}
- data["notify_level"] = "junk"
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
- t.Fatal("Should have errored - bad notify level")
+ data["desktop"] = model.CHANNEL_NOTIFY_ALL
+ data["mark_unread"] = "junk"
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
+ t.Fatal("Should have errored - bad mark unread level")
}
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
@@ -879,8 +918,9 @@ func TestUpdateNotifyLevel(t *testing.T) {
data["channel_id"] = channel1.Id
data["user_id"] = user2.Id
- data["notify_level"] = model.CHANNEL_NOTIFY_MENTION
- if _, err := Client.UpdateNotifyLevel(data); err == nil {
+ data["desktop"] = model.CHANNEL_NOTIFY_MENTION
+ data["mark_unread"] = model.CHANNEL_MARK_UNREAD_MENTION
+ if _, err := Client.UpdateNotifyProps(data); err == nil {
t.Fatal("Should have errored - user not in channel")
}
}
diff --git a/api/command.go b/api/command.go
index 0d2f7597b..427922344 100644
--- a/api/command.go
+++ b/api/command.go
@@ -195,7 +195,7 @@ func joinCommand(c *Context, command *model.Command) bool {
return false
}
- command.GotoLocation = "/channels/" + v.Name
+ command.GotoLocation = c.GetTeamURL() + "/channels/" + v.Name
command.Response = model.RESP_EXECUTED
return true
}
diff --git a/api/command_test.go b/api/command_test.go
index fe52dd41b..360c4da58 100644
--- a/api/command_test.go
+++ b/api/command_test.go
@@ -4,7 +4,9 @@
package api
import (
+ "strings"
"testing"
+ "time"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
@@ -126,12 +128,12 @@ func TestJoinCommands(t *testing.T) {
}
rs5 := Client.Must(Client.Command("", "/join "+channel2.Name, false)).Data.(*model.Command)
- if rs5.GotoLocation != "/channels/"+channel2.Name {
+ if !strings.HasSuffix(rs5.GotoLocation, "/"+team.Name+"/channels/"+channel2.Name) {
t.Fatal("failed to join channel")
}
rs6 := Client.Must(Client.Command("", "/join "+channel3.Name, false)).Data.(*model.Command)
- if rs6.GotoLocation == "/channels/"+channel3.Name {
+ if strings.HasSuffix(rs6.GotoLocation, "/"+team.Name+"/channels/"+channel3.Name) {
t.Fatal("should not have joined direct message channel")
}
@@ -175,6 +177,8 @@ func TestEchoCommand(t *testing.T) {
t.Fatal("Echo command failed to execute")
}
+ time.Sleep(100 * time.Millisecond)
+
p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
if len(p1.Order) != 1 {
t.Fatal("Echo command failed to send")
diff --git a/api/context.go b/api/context.go
index 02c3dc902..e80582b2a 100644
--- a/api/context.go
+++ b/api/context.go
@@ -137,7 +137,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if session == nil || session.IsExpired() {
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
} else if !session.IsOAuth && isTokenFromQueryString {
@@ -303,7 +303,6 @@ func (c *Context) HasSystemAdminPermissions(where string) bool {
}
func (c *Context) IsSystemAdmin() bool {
- // TODO XXX FIXME && IsPrivateIpAddress(c.IpAddress)
if model.IsInRole(c.Session.Roles, model.ROLE_SYSTEM_ADMIN) {
return true
}
@@ -317,7 +316,7 @@ func (c *Context) IsTeamAdmin() bool {
return false
}
-func (c *Context) RemoveSessionCookie(w http.ResponseWriter) {
+func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
sessionCache.Remove(c.Session.Token)
@@ -330,6 +329,21 @@ func (c *Context) RemoveSessionCookie(w http.ResponseWriter) {
}
http.SetCookie(w, cookie)
+
+ multiToken := ""
+ if oldMultiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil {
+ multiToken = oldMultiCookie.Value
+ }
+
+ multiCookie := &http.Cookie{
+ Name: model.MULTI_SESSION_TOKEN,
+ Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)),
+ Path: "/",
+ MaxAge: model.SESSION_TIME_WEB_IN_SECS,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, multiCookie)
}
func (c *Context) SetInvalidParam(where string, name string) {
@@ -346,7 +360,7 @@ func (c *Context) setTeamURL(url string, valid bool) {
c.teamURLValid = valid
}
-func (c *Context) setTeamURLFromSession() {
+func (c *Context) SetTeamURLFromSession() {
if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err == nil {
c.setTeamURL(c.GetSiteURL()+"/"+result.Data.(*model.Team).Name, true)
}
@@ -362,7 +376,7 @@ func (c *Context) GetTeamURLFromTeam(team *model.Team) string {
func (c *Context) GetTeamURL() string {
if !c.teamURLValid {
- c.setTeamURLFromSession()
+ c.SetTeamURLFromSession()
if !c.teamURLValid {
l4g.Debug("TeamURL accessed when not valid. Team URL should not be used in api functions or those that are team independent")
}
diff --git a/api/file.go b/api/file.go
index 5ed422811..5dc1db650 100644
--- a/api/file.go
+++ b/api/file.go
@@ -5,16 +5,15 @@ package api
import (
"bytes"
- "code.google.com/p/graphics-go/graphics"
l4g "code.google.com/p/log4go"
"fmt"
+ "github.com/disintegration/imaging"
"github.com/goamz/goamz/aws"
"github.com/goamz/goamz/s3"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
- "github.com/nfnt/resize"
"github.com/rwcarlsen/goexif/exif"
_ "golang.org/x/image/bmp"
"image"
@@ -24,7 +23,6 @@ import (
"image/jpeg"
"io"
"io/ioutil"
- "math"
"mime"
"net/http"
"net/url"
@@ -163,7 +161,7 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
name := filename[:strings.LastIndex(filename, ".")]
go func() {
// Decode image bytes into Image object
- img, _, err := image.Decode(bytes.NewReader(fileData[i]))
+ img, imgType, err := image.Decode(bytes.NewReader(fileData[i]))
if err != nil {
l4g.Error("Unable to decode image channelId=%v userId=%v filename=%v err=%v", channelId, userId, filename, err)
return
@@ -175,47 +173,30 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
// Get the image's orientation and ignore any errors since not all images will have orientation data
orientation, _ := getImageOrientation(fileData[i])
- // Create a temporary image that will be manipulated and then used to make the thumbnail and preview image
- var temp *image.RGBA
- switch orientation {
- case Upright, UprightMirrored, UpsideDown, UpsideDownMirrored:
- temp = image.NewRGBA(img.Bounds())
- case RotatedCCW, RotatedCCWMirrored, RotatedCW, RotatedCWMirrored:
- bounds := img.Bounds()
- temp = image.NewRGBA(image.Rect(bounds.Min.Y, bounds.Min.X, bounds.Max.Y, bounds.Max.X))
-
- width, height = height, width
+ if imgType == "png" {
+ dst := image.NewRGBA(img.Bounds())
+ draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
+ draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over)
+ img = dst
}
- // Draw a white background since JPEGs lack transparency
- draw.Draw(temp, temp.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
-
- // Copy the original image onto the temporary one while rotating it as necessary
switch orientation {
- case UpsideDown, UpsideDownMirrored:
- // rotate 180 degrees
- err := graphics.Rotate(temp, img, &graphics.RotateOptions{Angle: math.Pi})
- if err != nil {
- l4g.Error("Unable to rotate image")
- }
- case RotatedCW, RotatedCWMirrored:
- // rotate 90 degrees CCW
- graphics.Rotate(temp, img, &graphics.RotateOptions{Angle: 3 * math.Pi / 2})
- if err != nil {
- l4g.Error("Unable to rotate image")
- }
- case RotatedCCW, RotatedCCWMirrored:
- // rotate 90 degrees CW
- graphics.Rotate(temp, img, &graphics.RotateOptions{Angle: math.Pi / 2})
- if err != nil {
- l4g.Error("Unable to rotate image")
- }
- case Upright, UprightMirrored:
- draw.Draw(temp, temp.Bounds(), img, img.Bounds().Min, draw.Over)
+ case UprightMirrored:
+ img = imaging.FlipH(img)
+ case UpsideDown:
+ img = imaging.Rotate180(img)
+ case UpsideDownMirrored:
+ img = imaging.FlipV(img)
+ case RotatedCWMirrored:
+ img = imaging.Transpose(img)
+ case RotatedCCW:
+ img = imaging.Rotate270(img)
+ case RotatedCCWMirrored:
+ img = imaging.Transverse(img)
+ case RotatedCW:
+ img = imaging.Rotate90(img)
}
- img = temp
-
// Create thumbnail
go func() {
thumbWidth := float64(utils.Cfg.FileSettings.ThumbnailWidth)
@@ -227,9 +208,9 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
if imgHeight < thumbHeight && imgWidth < thumbWidth {
thumbnail = img
} else if imgHeight/imgWidth < thumbHeight/thumbWidth {
- thumbnail = resize.Resize(0, utils.Cfg.FileSettings.ThumbnailHeight, img, resize.Lanczos3)
+ thumbnail = imaging.Resize(img, 0, utils.Cfg.FileSettings.ThumbnailHeight, imaging.Lanczos)
} else {
- thumbnail = resize.Resize(utils.Cfg.FileSettings.ThumbnailWidth, 0, img, resize.Lanczos3)
+ thumbnail = imaging.Resize(img, utils.Cfg.FileSettings.ThumbnailWidth, 0, imaging.Lanczos)
}
buf := new(bytes.Buffer)
@@ -249,7 +230,7 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
go func() {
var preview image.Image
if width > int(utils.Cfg.FileSettings.PreviewWidth) {
- preview = resize.Resize(utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, img, resize.Lanczos3)
+ preview = imaging.Resize(img, utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, imaging.Lanczos)
} else {
preview = img
}
diff --git a/api/post.go b/api/post.go
index 2b683fb7d..65dad0eb6 100644
--- a/api/post.go
+++ b/api/post.go
@@ -88,6 +88,8 @@ func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.P
}
}
+ post.CreateAt = 0
+
post.Hashtags, _ = model.ParseHashtags(post.Message)
post.UserId = c.Session.UserId
diff --git a/api/team.go b/api/team.go
index cff34390a..8e5d634aa 100644
--- a/api/team.go
+++ b/api/team.go
@@ -75,7 +75,10 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- m["follow_link"] = bodyPage.Props["Link"]
+ if !utils.Cfg.EmailSettings.RequireEmailVerification {
+ m["follow_link"] = bodyPage.Props["Link"]
+ }
+
w.Header().Set("Access-Control-Allow-Origin", " *")
w.Write([]byte(model.MapToJson(m)))
}
diff --git a/api/user.go b/api/user.go
index a5c3fca2b..2d7dd9ab1 100644
--- a/api/user.go
+++ b/api/user.go
@@ -8,13 +8,13 @@ import (
l4g "code.google.com/p/log4go"
b64 "encoding/base64"
"fmt"
+ "github.com/disintegration/imaging"
"github.com/golang/freetype"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
- "github.com/nfnt/resize"
"hash/fnv"
"image"
"image/color"
@@ -394,6 +394,41 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User,
http.SetCookie(w, sessionCookie)
+ multiToken := ""
+ if originalMultiSessionCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil {
+ multiToken = originalMultiSessionCookie.Value
+ }
+
+ // Attempt to clean all the old tokens or duplicate tokens
+ if len(multiToken) > 0 {
+ tokens := strings.Split(multiToken, " ")
+
+ multiToken = ""
+ seen := make(map[string]string)
+ seen[session.TeamId] = session.TeamId
+ for _, token := range tokens {
+ if sr := <-Srv.Store.Session().Get(token); sr.Err == nil {
+ s := sr.Data.(*model.Session)
+ if !s.IsExpired() && seen[s.TeamId] == "" {
+ multiToken += " " + token
+ seen[s.TeamId] = s.TeamId
+ }
+ }
+ }
+ }
+
+ multiToken = strings.TrimSpace(session.Token + " " + multiToken)
+
+ multiSessionCookie := &http.Cookie{
+ Name: model.MULTI_SESSION_TOKEN,
+ Value: multiToken,
+ Path: "/",
+ MaxAge: maxAge,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, multiSessionCookie)
+
c.Session = *session
c.LogAuditWithUserId(user.Id, "success")
}
@@ -467,10 +502,14 @@ func RevokeAllSession(c *Context, userId string) {
for _, session := range sessions {
c.LogAuditWithUserId(userId, "session_id="+session.Id)
- sessionCache.Remove(session.Token)
- if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
- c.Err = result.Err
- return
+ if session.IsOAuth {
+ RevokeAccessToken(session.Token)
+ } else {
+ sessionCache.Remove(session.Token)
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
}
}
}
@@ -510,7 +549,7 @@ func logout(c *Context, w http.ResponseWriter, r *http.Request) {
func Logout(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("")
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
if result := <-Srv.Store.Session().Remove(c.Session.Id); result.Err != nil {
c.Err = result.Err
return
@@ -525,7 +564,7 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) {
if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = result.Err
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId)
return
} else if HandleEtag(result.Data.(*model.User).Etag(), w, r) {
@@ -799,7 +838,7 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
}
// Scale profile image
- img = resize.Resize(utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, img, resize.Lanczos3)
+ img = imaging.Resize(img, utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, imaging.Lanczos)
buf := new(bytes.Buffer)
err = png.Encode(buf, img)
@@ -994,7 +1033,7 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
sessions := result.Data.([]*model.Session)
for _, s := range sessions {
- sessionCache.Remove(s.Id)
+ sessionCache.Remove(s.Token)
}
}
@@ -1417,7 +1456,7 @@ func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, te
func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, *model.AppError) {
sso := utils.Cfg.GetSSOService(service)
- if sso != nil && !sso.Enable {
+ if sso == nil || !sso.Enable {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service)
}
@@ -1459,6 +1498,9 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error())
} else {
ar = model.AccessResponseFromJson(resp.Body)
+ if ar == nil {
+ return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad response from token request", "")
+ }
}
if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE {
diff --git a/config/config.json b/config/config.json
index 60e197154..b14175372 100644
--- a/config/config.json
+++ b/config/config.json
@@ -5,7 +5,9 @@
"SegmentDeveloperKey": "",
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
- "EnableIncomingWebhooks": false,
+ "EnableIncomingWebhooks": true,
+ "EnablePostUsernameOverride": false,
+ "EnablePostIconOverride": false,
"EnableTesting": false
},
"TeamSettings": {
@@ -75,7 +77,8 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowFullName": true
+ "ShowFullName": true,
+ "EnableDiagnostic": false
},
"GitLabSettings": {
"Enable": false,
@@ -86,4 +89,4 @@
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
-} \ No newline at end of file
+}
diff --git a/doc/README.md b/doc/README.md
index 6bf96492a..d711b5d53 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,25 +1,26 @@
# Mattermost Documentation
-## General Information
+## Installation
-- [Mattermost Release Numbering Scheme](install/release-numbering.md)
+- [AWS Elastic Beanstalk Setup](install/Amazon-Elastic-Beanstalk.md)
+- [Docker Single Container Preview Setup](install/Docker-Single-Container.md)
+- [Production Ubuntu Setup](install/Production-Ubuntu.md)
+- [Mattermost Release Numbering Scheme](install/Release-Numbering.md)
+- [Software and Hardware Requirements](install/Requirements.md)
+- [SMTP Email Setup](install/SMTP-Email-Setup.md)
-## Administrator Documentation
+## Integrations
-### Installation
+- [GitLab SSO Configuration](integrations/Single-Sign-On/Gitlab.md)
+- [Incoming Webhooks](integrations/webhooks/Incoming.md)
-- [Software and Hardware Requirements](install/requirements.md)
-- [Local Machine Setup ](install/single-container-install.md)
-- [AWS Elastic Beanstalk Setup](install/aws-ebs-setup.md)
-- [Developer Machine Setup](install/dev-setup.md)
-
-### Configuration
+## Developer Documentation
-- [GitLab SSO Configuration](integrations/sso/gitlab-sso.md)
-- [SMTP Email Setup](config/smtp-email-setup.md)
+- [Code Contribution Guidelines](developer/Code-Contribution-Guidelines.md)
+- [Developer Machine Setup](developer/Setup.md)
+- [Mattermost Style Guide](developer/Style-Guide.md)
-## Developer Documentation
+## Usage Help
-- [Code Contribution Guidelines](developer/code-contribution.md)
-- [Developer Machine Setup](install/dev-setup.md)
-- [Mattermost Style Guide](developer/style-guide.md)
+- [Slack Import](usage/Slack-Import.md)
+- [Mattermost Markdown Formatting](usage/Markdown.md)
diff --git a/doc/developer/code-contribution.md b/doc/developer/Code-Contribution-Guidelines.md
index 51521be1b..80676f107 100644
--- a/doc/developer/code-contribution.md
+++ b/doc/developer/Code-Contribution-Guidelines.md
@@ -23,7 +23,7 @@ git checkout -b <branch name>
## Programming and Testing
-1. Please review the [Mattermost Style Guide](style-guide.md) prior to making changes.
+1. Please review the [Mattermost Style Guide](Style-Guide.md) prior to making changes.
To keep code clean and well structured, Mattermost uses ESLint to check that pull requests adhere to style guidelines for React. Code will need to follow Mattermost's React style guidelines in order to pass the automated build tests when a pull request is submitted.
diff --git a/doc/install/dev-setup.md b/doc/developer/Setup.md
index b90b6a351..c63bde512 100644
--- a/doc/install/dev-setup.md
+++ b/doc/developer/Setup.md
@@ -3,13 +3,14 @@ Developer Machine Setup
### Mac OS X ###
-1. Download and set up Boot2Docker
+1. Download and set up Docker Toolbox
1. Follow the instructions at http://docs.docker.com/installation/mac/
- 1. Use the Boot2Docker command-line utility
- 2. If you do command-line setup use: `boot2docker init eval “$(boot2docker shellinit)”`
- 2. Get your Docker IP address with `boot2docker ip`
+ 2. Start a new docker host
+ `docker-machine create -d virtualbox dev`
+ 2. Get the IP address of your docker host
+ `docker-machine ip dev`
3. Add a line to your /etc/hosts that goes `<Docker IP> dockerhost`
- 4. Run `boot2docker shellinit` and copy the export statements to your ~/.bash_profile
+ 4. Run `docker-machine env dev` and copy the export statements to your ~/.bash_profile
2. Download Go (version 1.4.2) from http://golang.org/dl/
3. Set up your Go workspace
1. `mkdir ~/go`
diff --git a/doc/developer/style-guide.md b/doc/developer/Style-Guide.md
index a06f9e29b..a06f9e29b 100644
--- a/doc/developer/style-guide.md
+++ b/doc/developer/Style-Guide.md
diff --git a/doc/favicon.ico b/doc/favicon.ico
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/doc/favicon.ico
diff --git a/doc/install/aws-ebs-setup.md b/doc/install/Amazon-Elastic-Beanstalk.md
index e186fa9c1..0416b67ea 100644
--- a/doc/install/aws-ebs-setup.md
+++ b/doc/install/Amazon-Elastic-Beanstalk.md
@@ -1,14 +1,14 @@
## AWS Elastic Beanstalk Setup (Docker)
-1. Create a new Elastic Beanstalk Docker application using the [Dockerrun.aws.zip](/docker/0.7/Dockerrun.aws.zip) file provided.
+1. Create a new Elastic Beanstalk Docker application using the [Dockerrun.aws.zip](https://github.com/mattermost/platform/raw/master/docker/1.0/Dockerrun.aws.zip) file provided.
1. From the AWS console select Elastic Beanstalk.
2. Select "Create New Application" from the top right.
3. Name the application and press next.
4. Select "Create a web server" environment.
5. If asked, select create an IAM role and instance profile and press next.
6. For predefined configuration select under Generic: Docker. For environment type select single instance.
- 7. For application source, select upload your own and upload Dockerrun.aws.zip from [Dockerrun.aws.zip](/docker/0.7/Dockerrun.aws.zip). Everything else may be left at default.
+ 7. For application source, select upload your own and upload Dockerrun.aws.zip from [Dockerrun.aws.zip](https://github.com/mattermost/platform/raw/master/docker/1.0/Dockerrun.aws.zip). Everything else may be left at default.
8. Select an environment name, this is how you will refer to your environment. Make sure the URL is available then press next.
9. The options on the additional resources page may be left at default unless you wish to change them. Press Next.
10. On the configuration details place. Select an instance type of t2.small or larger.
@@ -24,4 +24,4 @@
### (Recommended) Enable Email
The default single-container Docker instance for Mattermost is designed for product evaluation, and sets `ByPassEmail=true` so the product can run without enabling email, when doing so maybe difficult.
- To see the product's full functionality, [enabling SMTP email is recommended](doc/config/smtp-email-setup.md).
+ To see the product's full functionality, [enabling SMTP email is recommended](SMTP-Email-Setup.md).
diff --git a/doc/install/single-container-install.md b/doc/install/Docker-Single-Container.md
index 772f3becf..4b952cd71 100644
--- a/doc/install/single-container-install.md
+++ b/doc/install/Docker-Single-Container.md
@@ -4,11 +4,11 @@ The following install instructions are for single-container installs of Mattermo
### Mac OSX ###
-1. Install Boot2Docker using instructions at: http://docs.docker.com/installation/mac/
- 1. Start Boot2Docker from the command line and run: `boot2docker init eval “$(boot2docker shellinit)”`
-2. Get your Docker IP address with: `boot2docker ip`
+1. Install Docker Toolbox using instructions at: http://docs.docker.com/installation/mac/
+ 1. Start Docker Toolbox from the command line and run: `docker-machine create -d virtualbox dev`
+2. Get your Docker IP address with: `docker-machine ip dev`
3. Use `sudo nano /etc/hosts` to add `<Docker IP> dockerhost` to your /etc/hosts file
-4. Run: `boot2docker shellinit` and copy the export statements to your ~/.bash\_profile by running `sudo nano ~/.bash_profile`. Then run: `source ~/.bash_profile`
+4. Run: `docker-machine env dev` and copy the export statements to your ~/.bash\_profile by running `sudo nano ~/.bash_profile`. Then run: `source ~/.bash_profile`
5. Run: `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
6. When docker is done fetching the image, open http://dockerhost:8065/ in your browser.
@@ -75,7 +75,7 @@ The following install instructions are for single-container installs of Mattermo
## Configuration Settings
-There are a few configuration settings you might want to adjust when setting up your instance of Mattermost. You can edit them in [config/config.json](config/config.json) or [docker/0.6/config_docker.json](docker/0.6/config_docker.json) if you're running a Docker instance.
+There are a few configuration settings you might want to adjust when setting up your instance of Mattermost. You can edit them in `config.json` or `config_docker.json` if you're running a Docker instance.
* *EmailSettings*:*ByPassEmail* - If this is set to true, then users on the system will not need to verify their email addresses when signing up. In addition, no emails will ever be sent.
* *ServiceSettings*:*UseLocalStorage* - If this is set to true, then your Mattermost server will store uploaded files in the storage directory specified by *StorageDirectory*. *StorageDirectory* must be set if *UseLocalStorage* is set to true.
@@ -86,7 +86,7 @@ There are a few configuration settings you might want to adjust when setting up
The default single-container Docker instance for Mattermost is designed for product evaluation, and sets `ByPassEmail=true` so the product can run without enabling email, when doing so maybe difficult.
-To see the product's full functionality, [enabling SMTP email is recommended](/doc/config/smtp-email-setup.md).
+To see the product's full functionality, [enabling SMTP email is recommended](SMTP-Email-Setup.md).
## Upgrading Mattermost
diff --git a/doc/install/prod-ubuntu.md b/doc/install/Production-Ubuntu.md
index 866b1bdbe..d2490062d 100644
--- a/doc/install/prod-ubuntu.md
+++ b/doc/install/Production-Ubuntu.md
@@ -6,7 +6,7 @@
* ``` sudo apt-get update```
* ``` sudo apt-get upgrade```
-## Setup Database Server
+## Set up Database Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.1
1. Install PostgreSQL 9.3+ (or MySQL 5.2+)
* ``` sudo apt-get install postgresql postgresql-contrib```
@@ -25,13 +25,13 @@
1. You can exit the postgres account by typing:
* ``` exit```
-## Setup Mattermost Server
+## Set up Mattermost Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.2
1. Download the latest Mattermost Server by typing:
* ``` wget https://github.com/mattermost/platform/releases/download/v1.0.0/mattermost.tar.gz```
1. Unzip the Mattermost Server by typing:
* ``` tar -xvzf mattermost.tar.gz```
-1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`, in the future we will give guidance for storing under `/opt`.
+1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`. In the future we will give guidance for storing under `/opt`.
1. We have also elected to run the Mattermost Server as the `ubuntu` account for simplicity. We recommend settings up and running the service under a `mattermost` user account with limited permissions.
1. Create the storage directory for files. We assume you will have attached a large drive for storage of images and files. For this setup we will assume the directory is located at `/mattermost/data`.
* Create the directory by typing:
@@ -70,7 +70,7 @@ exec bin/platform
* You should see a page titles *Mattermost - Signup*
* You can also stop the process by running the command ` sudo stop mattermost`, but we will skip this step for now.
-## Setup Nginx Server
+## Set up Nginx Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.3
1. We use Nginx for proxying request to the Mattermost Server. The main benefits are:
* SSL termination
@@ -90,8 +90,7 @@ exec bin/platform
1. Configure Nginx to proxy connections from the internet to the Mattermost Server
* Create a configuration for Mattermost
* ``` sudo touch /etc/nginx/sites-available/mattermost```
- * Below is a sample configuration with the minimum settings required to configure Mattermost.
- *
+ * Below is a sample configuration with the minimum settings required to configure Mattermost
```
server {
server_name mattermost.example.com;
@@ -118,7 +117,7 @@ exec bin/platform
* ``` curl http://localhost```
* You should see a page titles *Mattermost - Signup*
-## Setup Nginx with SSL (Recommended)
+## Set up Nginx with SSL (Recommended)
1. You will need a SSL cert from a certificate authority.
1. For simplicity we will generate a test certificate.
* ``` mkdir ~/cert```
@@ -177,9 +176,9 @@ exec bin/platform
* Save the Settings
1. Update File Settings
* Change *Local Directory Location* from `./data/` to `/mattermost/data`
-1. Update Log Settings
+1. Update Log Settings.
* Set *Log to The Console* to false
-1. Update Rate Limit Settings
+1. Update Rate Limit Settings.
* Set *Vary By Remote Address* to false
* Set *Vary By HTTP Header* to X-Real-IP
1. Feel free to modify other settings.
diff --git a/doc/install/release-numbering.md b/doc/install/Release-Numbering.md
index 71374f7ef..71374f7ef 100644
--- a/doc/install/release-numbering.md
+++ b/doc/install/Release-Numbering.md
diff --git a/doc/install/requirements.md b/doc/install/Requirements.md
index 1e0a12fb9..1e0a12fb9 100644
--- a/doc/install/requirements.md
+++ b/doc/install/Requirements.md
diff --git a/doc/config/smtp-email-setup.md b/doc/install/SMTP-Email-Setup.md
index 86e2bb20e..86e2bb20e 100644
--- a/doc/config/smtp-email-setup.md
+++ b/doc/install/SMTP-Email-Setup.md
diff --git a/doc/integrations/sso/gitlab-sso.md b/doc/integrations/Single-Sign-On/Gitlab.md
index 6110db504..6110db504 100644
--- a/doc/integrations/sso/gitlab-sso.md
+++ b/doc/integrations/Single-Sign-On/Gitlab.md
diff --git a/doc/integrations/webhooks/Incoming.md b/doc/integrations/webhooks/Incoming.md
new file mode 100644
index 000000000..6e25f182e
--- /dev/null
+++ b/doc/integrations/webhooks/Incoming.md
@@ -0,0 +1,62 @@
+# Incoming Webhooks
+
+With incoming webhooks practically any external source - once given a URL by you - can post a message to any channel you have access to. This is done through a HTTP POST request with a simple JSON payload. The payload can contain some text, and some simple options to allow the external source to customize the post.
+
+## Creating the Webhook URL
+
+To get the incoming webhook URL - where all the HTTP requests will be sent - follow these steps:
+
+1. Login to your Mattermost account.
+2. Open the menu by clicking near your profile picture in the top-left and open Account Settings.
+3. Go to the Integrations tab and click the 'Edit' button next to 'Incoming Webhooks'.
+4. Use the selector to choose a channel and click the 'Add' button to create the webhook.
+5. Your webhook URL will be displayed below in the 'Existing incoming webhooks' section.
+
+
+## Posting a Message
+
+You can send the message by including a JSON string as the `payload` parameter in a HTTP POST request.
+```
+payload={"text": "Hello, this is some text."}
+```
+
+In addition, if `Content-Type` is specified as `application/json` in the headers of the HTTP request then the body of the request can be direct JSON.
+```
+{"text": "Hello, this is some text."}
+```
+
+It is also possible to post richly formatted messages using [Markdown](../../usage/Markdown.md).
+```
+payload={"text": "# A Header\nThe _text_ below **the** header."}
+```
+
+Just like regular posts, the text will be limited to 4000 characters at maximum.
+
+## Adding Links
+
+In addition to including links in the standard Markdown format, links can also be specified by enclosing the URL in `<>` brackets
+```
+payload={"text": "<http://www.mattermost.com/>"}
+```
+
+They can also include a `|` character to specify some clickable text.
+```
+payload={"text": "Click <http://www.mattermost.com/|here> for a link."}
+```
+
+## Channel Override
+
+You can use a single webhook URL to post messages to different channels by overriding the channel. You can do this by adding the channel name - as it is seen in the channel URL - to the request payload.
+```
+payload={"channel": "off-topic", "text": "Hello, this is some text."}
+```
+
+## Finishing up
+
+Combining everything above, here is an example message made using a curl command:
+
+```
+curl -i -X POST 'payload={"channel": "off-topic", "text": "Hello, this is some text."}' http://yourmattermost.com/hooks/xxxxxxxxxxxxxxxxxxxxxxxxxx
+```
+
+A post with that text will be made to the Off-Topic channel.
diff --git a/doc/help/enduser/markdown.md b/doc/usage/Markdown.md
index 9e2342a0b..9e2342a0b 100644
--- a/doc/help/enduser/markdown.md
+++ b/doc/usage/Markdown.md
diff --git a/doc/usage/Slack-Import.md b/doc/usage/Slack-Import.md
new file mode 100644
index 000000000..c30de0567
--- /dev/null
+++ b/doc/usage/Slack-Import.md
@@ -0,0 +1,18 @@
+#### Slack Import (Preview)
+
+*Note: As a SaaS service, Slack is able to change its export format quickly. If you encounter issues not mentioned in the documentation below, please let us know by [filing an issue](https://github.com/mattermost/platform/issues).*
+
+The Slack Import feature in Mattermost is in "Preview" and focus is on supporting migration of teams of less than 100 registered users. The feature can be accessed from by Team Administrators and Team Owners via the `Team Settings -> Import` menu option.
+
+Mattermost currently supports the processing of an "Export" file from Slack containing account information and public channel archives from a Slack team.
+
+- In the feature preview, emails and usernames from Slack are used to create new Mattermost accounts, connected to messages history in imported Slack channels. Users can activate these accounts and by going to the Password Reset screen in Mattermost to set new credentials.
+- Once logged in, users will have access to previous Slack messages shared in public channels, now imported to Mattermost.
+
+Limitations:
+
+- Newly added markdown suppport in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported.
+- Slack does not export files or images your team has stored in Slack's database. Mattermost will provide links to the location of your assets in Slack's web UI.
+- Slack does not export any content from private groups or direct messages that your team has stored in Slack's database.
+- The Preview release of Slack Import does not offer pre-checks or roll-back and will not import Slack accounts with username or email address collisions with existing Mattermost accounts. Also, Slack channel names with underscores will not import. Also, mentions do not yet resolve as Mattermost usernames (still show Slack ID).
+
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index 806071376..ef91a21ea 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -5,7 +5,9 @@
"SegmentDeveloperKey": "",
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
- "EnableIncomingWebhooks": false,
+ "EnableIncomingWebhooks": true,
+ "EnablePostUsernameOverride": false,
+ "EnablePostIconOverride": false,
"EnableTesting": false
},
"TeamSettings": {
@@ -75,7 +77,8 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowFullName": true
+ "ShowFullName": true,
+ "EnableDiagnostic": false
},
"GitLabSettings": {
"Enable": false,
@@ -86,4 +89,4 @@
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
-} \ No newline at end of file
+}
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index 806071376..ef91a21ea 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -5,7 +5,9 @@
"SegmentDeveloperKey": "",
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
- "EnableIncomingWebhooks": false,
+ "EnableIncomingWebhooks": true,
+ "EnablePostUsernameOverride": false,
+ "EnablePostIconOverride": false,
"EnableTesting": false
},
"TeamSettings": {
@@ -75,7 +77,8 @@
},
"PrivacySettings": {
"ShowEmailAddress": true,
- "ShowFullName": true
+ "ShowFullName": true,
+ "EnableDiagnostic": false
},
"GitLabSettings": {
"Enable": false,
@@ -86,4 +89,4 @@
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
-} \ No newline at end of file
+}
diff --git a/mattermost.go b/mattermost.go
index ff7b0f3f3..e78e8d04a 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -8,6 +8,8 @@ import (
"fmt"
"os"
"os/signal"
+ "runtime"
+ "strconv"
"strings"
"syscall"
"time"
@@ -61,6 +63,8 @@ func main() {
manualtesting.InitManualTesting()
}
+ diagnosticsJob()
+
// wait for kill signal before attempting to gracefully shutdown
// the running service
c := make(chan os.Signal)
@@ -71,6 +75,53 @@ func main() {
}
}
+func diagnosticsJob() {
+ go func() {
+ for {
+ if utils.Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() {
+ if result := <-api.Srv.Store.System().Get(); result.Err == nil {
+ props := result.Data.(model.StringMap)
+ lastTime, _ := strconv.ParseInt(props["LastDiagnosticTime"], 10, 0)
+ currentTime := model.GetMillis()
+
+ if (currentTime - lastTime) > 1000*60*60*24*7 {
+ l4g.Info("Sending error and diagnostic information to mattermost")
+
+ id := props["DiagnosticId"]
+ if len(id) == 0 {
+ id = model.NewId()
+ systemId := &model.System{Name: "DiagnosticId", Value: id}
+ <-api.Srv.Store.System().Save(systemId)
+ }
+
+ systemLastTime := &model.System{Name: "LastDiagnosticTime", Value: strconv.FormatInt(currentTime, 10)}
+ if lastTime == 0 {
+ <-api.Srv.Store.System().Save(systemLastTime)
+ } else {
+ <-api.Srv.Store.System().Update(systemLastTime)
+ }
+
+ m := make(map[string]string)
+ m[utils.PROP_DIAGNOSTIC_ID] = id
+ m[utils.PROP_DIAGNOSTIC_BUILD] = model.CurrentVersion + "." + model.BuildNumber
+ m[utils.PROP_DIAGNOSTIC_DATABASE] = utils.Cfg.SqlSettings.DriverName
+ m[utils.PROP_DIAGNOSTIC_OS] = runtime.GOOS
+ m[utils.PROP_DIAGNOSTIC_CATEGORY] = utils.VAL_DIAGNOSTIC_CATEGORY_DEFALUT
+
+ if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil {
+ m[utils.PROP_DIAGNOSTIC_USER_COUNT] = strconv.FormatInt(ucr.Data.(int64), 10)
+ }
+
+ utils.SendDiagnostic(m)
+ }
+ }
+ }
+
+ time.Sleep(time.Hour * 24)
+ }
+ }()
+}
+
func parseCmds() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, usage)
@@ -355,7 +406,7 @@ Usage:
-reset_password Resets the password for a user. It requires the
-team_name, -email and -password flag.
Example:
- platform -reset_password -team_name="name" -email="user@example.com" -paossword="newpassword"
+ platform -reset_password -team_name="name" -email="user@example.com" -password="newpassword"
`
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 000000000..d7113e03e
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,11 @@
+site_name: Mattermost Documentation
+site_url: http://docs.mattermost.org
+repo_url: https://github.com/mattermost/platform
+repo_name: GitHub
+site_favicon: favicon.ico
+copyright: "Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved."
+strict: true
+docs_dir: doc
+site_dir: documentation-html
+use_directory_urls: false
+theme: mkdocs
diff --git a/model/channel_member.go b/model/channel_member.go
index 50f51304b..3ae612700 100644
--- a/model/channel_member.go
+++ b/model/channel_member.go
@@ -10,22 +10,24 @@ import (
)
const (
- CHANNEL_ROLE_ADMIN = "admin"
- CHANNEL_NOTIFY_ALL = "all"
- CHANNEL_NOTIFY_MENTION = "mention"
- CHANNEL_NOTIFY_NONE = "none"
- CHANNEL_NOTIFY_QUIET = "quiet"
+ CHANNEL_ROLE_ADMIN = "admin"
+ CHANNEL_NOTIFY_DEFAULT = "default"
+ CHANNEL_NOTIFY_ALL = "all"
+ CHANNEL_NOTIFY_MENTION = "mention"
+ CHANNEL_NOTIFY_NONE = "none"
+ CHANNEL_MARK_UNREAD_ALL = "all"
+ CHANNEL_MARK_UNREAD_MENTION = "mention"
)
type ChannelMember struct {
- ChannelId string `json:"channel_id"`
- UserId string `json:"user_id"`
- Roles string `json:"roles"`
- LastViewedAt int64 `json:"last_viewed_at"`
- MsgCount int64 `json:"msg_count"`
- MentionCount int64 `json:"mention_count"`
- NotifyLevel string `json:"notify_level"`
- LastUpdateAt int64 `json:"last_update_at"`
+ ChannelId string `json:"channel_id"`
+ UserId string `json:"user_id"`
+ Roles string `json:"roles"`
+ LastViewedAt int64 `json:"last_viewed_at"`
+ MsgCount int64 `json:"msg_count"`
+ MentionCount int64 `json:"mention_count"`
+ NotifyProps StringMap `json:"notify_props"`
+ LastUpdateAt int64 `json:"last_update_at"`
}
func (o *ChannelMember) ToJson() string {
@@ -64,8 +66,14 @@ func (o *ChannelMember) IsValid() *AppError {
}
}
- if len(o.NotifyLevel) > 20 || !IsChannelNotifyLevelValid(o.NotifyLevel) {
- return NewAppError("ChannelMember.IsValid", "Invalid notify level", "notify_level="+o.NotifyLevel)
+ notifyLevel := o.NotifyProps["desktop"]
+ if len(notifyLevel) > 20 || !IsChannelNotifyLevelValid(notifyLevel) {
+ return NewAppError("ChannelMember.IsValid", "Invalid notify level", "notify_level="+notifyLevel)
+ }
+
+ markUnreadLevel := o.NotifyProps["mark_unread"]
+ if len(markUnreadLevel) > 20 || !IsChannelMarkUnreadLevelValid(markUnreadLevel) {
+ return NewAppError("ChannelMember.IsValid", "Invalid mark unread level", "mark_unread_level="+markUnreadLevel)
}
return nil
@@ -75,6 +83,24 @@ func (o *ChannelMember) PreSave() {
o.LastUpdateAt = GetMillis()
}
+func (o *ChannelMember) PreUpdate() {
+ o.LastUpdateAt = GetMillis()
+}
+
func IsChannelNotifyLevelValid(notifyLevel string) bool {
- return notifyLevel == CHANNEL_NOTIFY_ALL || notifyLevel == CHANNEL_NOTIFY_MENTION || notifyLevel == CHANNEL_NOTIFY_NONE || notifyLevel == CHANNEL_NOTIFY_QUIET
+ return notifyLevel == CHANNEL_NOTIFY_DEFAULT ||
+ notifyLevel == CHANNEL_NOTIFY_ALL ||
+ notifyLevel == CHANNEL_NOTIFY_MENTION ||
+ notifyLevel == CHANNEL_NOTIFY_NONE
+}
+
+func IsChannelMarkUnreadLevelValid(markUnreadLevel string) bool {
+ return markUnreadLevel == CHANNEL_MARK_UNREAD_ALL || markUnreadLevel == CHANNEL_MARK_UNREAD_MENTION
+}
+
+func GetDefaultChannelNotifyProps() StringMap {
+ return StringMap{
+ "desktop": CHANNEL_NOTIFY_DEFAULT,
+ "mark_unread": CHANNEL_MARK_UNREAD_ALL,
+ }
}
diff --git a/model/channel_member_test.go b/model/channel_member_test.go
index 3b64ffbf7..edbb46e9b 100644
--- a/model/channel_member_test.go
+++ b/model/channel_member_test.go
@@ -31,24 +31,34 @@ func TestChannelMemberIsValid(t *testing.T) {
}
o.Roles = "missing"
- o.NotifyLevel = CHANNEL_NOTIFY_ALL
+ o.NotifyProps = GetDefaultChannelNotifyProps()
o.UserId = NewId()
if err := o.IsValid(); err == nil {
t.Fatal("should be invalid")
}
o.Roles = CHANNEL_ROLE_ADMIN
- o.NotifyLevel = "junk"
+ o.NotifyProps["desktop"] = "junk"
if err := o.IsValid(); err == nil {
t.Fatal("should be invalid")
}
- o.NotifyLevel = "123456789012345678901"
+ o.NotifyProps["desktop"] = "123456789012345678901"
if err := o.IsValid(); err == nil {
t.Fatal("should be invalid")
}
- o.NotifyLevel = CHANNEL_NOTIFY_ALL
+ o.NotifyProps["desktop"] = CHANNEL_NOTIFY_ALL
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+
+ o.NotifyProps["mark_unread"] = "123456789012345678901"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.NotifyProps["mark_unread"] = CHANNEL_MARK_UNREAD_ALL
if err := o.IsValid(); err != nil {
t.Fatal(err)
}
diff --git a/model/client.go b/model/client.go
index 26e00864d..a291cc4f2 100644
--- a/model/client.go
+++ b/model/client.go
@@ -450,8 +450,8 @@ func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError)
}
}
-func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) {
- if r, err := c.DoApiPost("/channels/update_notify_level", MapToJson(data)); err != nil {
+func (c *Client) UpdateNotifyProps(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/channels/update_notify_props", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
diff --git a/model/config.go b/model/config.go
index 31c2b16dc..c67b36063 100644
--- a/model/config.go
+++ b/model/config.go
@@ -16,6 +16,9 @@ const (
IMAGE_DRIVER_LOCAL = "local"
IMAGE_DRIVER_S3 = "amazons3"
+ DATABASE_DRIVER_MYSQL = "mysql"
+ DATABASE_DRIVER_POSTGRES = "postgres"
+
SERVICE_GITLAB = "gitlab"
)
@@ -26,6 +29,8 @@ type ServiceSettings struct {
GoogleDeveloperKey string
EnableOAuthServiceProvider bool
EnableIncomingWebhooks bool
+ EnablePostUsernameOverride bool
+ EnablePostIconOverride bool
EnableTesting bool
}
@@ -63,12 +68,12 @@ type FileSettings struct {
Directory string
EnablePublicLink bool
PublicLinkSalt string
- ThumbnailWidth uint
- ThumbnailHeight uint
- PreviewWidth uint
- PreviewHeight uint
- ProfileWidth uint
- ProfileHeight uint
+ ThumbnailWidth int
+ ThumbnailHeight int
+ PreviewWidth int
+ PreviewHeight int
+ ProfileWidth int
+ ProfileHeight int
InitialFont string
AmazonS3AccessKeyId string
AmazonS3SecretAccessKey string
@@ -107,6 +112,7 @@ type RateLimitSettings struct {
type PrivacySettings struct {
ShowEmailAddress bool
ShowFullName bool
+ EnableDiagnostic bool
}
type TeamSettings struct {
@@ -156,3 +162,92 @@ func ConfigFromJson(data io.Reader) *Config {
return nil
}
}
+
+func (o *Config) IsValid() *AppError {
+
+ if o.ServiceSettings.MaximumLoginAttempts <= 0 {
+ return NewAppError("Config.IsValid", "Invalid maximum login attempts for service settings. Must be a positive number.", "")
+ }
+
+ if len(o.ServiceSettings.ListenAddress) == 0 {
+ return NewAppError("Config.IsValid", "Invalid listen address for service settings Must be set.", "")
+ }
+
+ if o.TeamSettings.MaxUsersPerTeam <= 0 {
+ return NewAppError("Config.IsValid", "Invalid maximum users per team for team settings. Must be a positive number.", "")
+ }
+
+ if len(o.SqlSettings.AtRestEncryptKey) != 32 {
+ return NewAppError("Config.IsValid", "Invalid at rest encrypt key for SQL settings. Must be 32 chars.", "")
+ }
+
+ if !(o.SqlSettings.DriverName == DATABASE_DRIVER_MYSQL || o.SqlSettings.DriverName == DATABASE_DRIVER_POSTGRES) {
+ return NewAppError("Config.IsValid", "Invalid driver name for SQL settings. Must be 'mysql' or 'postgres'", "")
+ }
+
+ if o.SqlSettings.MaxIdleConns <= 0 {
+ return NewAppError("Config.IsValid", "Invalid maximum idle connection for SQL settings. Must be a positive number.", "")
+ }
+
+ if len(o.SqlSettings.DataSource) == 0 {
+ return NewAppError("Config.IsValid", "Invalid data source for SQL settings. Must be set.", "")
+ }
+
+ if o.SqlSettings.MaxOpenConns <= 0 {
+ return NewAppError("Config.IsValid", "Invalid maximum open connection for SQL settings. Must be a positive number.", "")
+ }
+
+ if !(o.FileSettings.DriverName == IMAGE_DRIVER_LOCAL || o.FileSettings.DriverName == IMAGE_DRIVER_S3) {
+ return NewAppError("Config.IsValid", "Invalid driver name for file settings. Must be 'local' or 'amazons3'", "")
+ }
+
+ if o.FileSettings.PreviewHeight < 0 {
+ return NewAppError("Config.IsValid", "Invalid preview height for file settings. Must be a zero or positive number.", "")
+ }
+
+ if o.FileSettings.PreviewWidth <= 0 {
+ return NewAppError("Config.IsValid", "Invalid preview width for file settings. Must be a positive number.", "")
+ }
+
+ if o.FileSettings.ProfileHeight <= 0 {
+ return NewAppError("Config.IsValid", "Invalid profile height for file settings. Must be a positive number.", "")
+ }
+
+ if o.FileSettings.ProfileWidth <= 0 {
+ return NewAppError("Config.IsValid", "Invalid profile width for file settings. Must be a positive number.", "")
+ }
+
+ if o.FileSettings.ThumbnailHeight <= 0 {
+ return NewAppError("Config.IsValid", "Invalid thumbnail height for file settings. Must be a positive number.", "")
+ }
+
+ if o.FileSettings.ThumbnailHeight <= 0 {
+ return NewAppError("Config.IsValid", "Invalid thumbnail width for file settings. Must be a positive number.", "")
+ }
+
+ if len(o.FileSettings.PublicLinkSalt) != 32 {
+ return NewAppError("Config.IsValid", "Invalid public link salt for file settings. Must be 32 chars.", "")
+ }
+
+ if !(o.EmailSettings.ConnectionSecurity == CONN_SECURITY_NONE || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_TLS || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_STARTTLS) {
+ return NewAppError("Config.IsValid", "Invalid connection security for email settings. Must be '', 'TLS', or 'STARTTLS'", "")
+ }
+
+ if len(o.EmailSettings.InviteSalt) != 32 {
+ return NewAppError("Config.IsValid", "Invalid invite salt for email settings. Must be 32 chars.", "")
+ }
+
+ if len(o.EmailSettings.PasswordResetSalt) != 32 {
+ return NewAppError("Config.IsValid", "Invalid password reset salt for email settings. Must be 32 chars.", "")
+ }
+
+ if o.RateLimitSettings.MemoryStoreSize <= 0 {
+ return NewAppError("Config.IsValid", "Invalid memory store size for rate limit settings. Must be a positive number", "")
+ }
+
+ if o.RateLimitSettings.PerSec <= 0 {
+ return NewAppError("Config.IsValid", "Invalid per sec for rate limit settings. Must be a positive number", "")
+ }
+
+ return nil
+}
diff --git a/model/post.go b/model/post.go
index e78469940..1fc5963c3 100644
--- a/model/post.go
+++ b/model/post.go
@@ -120,7 +120,10 @@ func (o *Post) PreSave() {
o.OriginalId = ""
- o.CreateAt = GetMillis()
+ if o.CreateAt == 0 {
+ o.CreateAt = GetMillis()
+ }
+
o.UpdateAt = o.CreateAt
if o.Props == nil {
diff --git a/model/post_test.go b/model/post_test.go
index 38f4b4c98..a6b880fa0 100644
--- a/model/post_test.go
+++ b/model/post_test.go
@@ -83,5 +83,18 @@ func TestPostIsValid(t *testing.T) {
func TestPostPreSave(t *testing.T) {
o := Post{Message: "test"}
o.PreSave()
+
+ if o.CreateAt == 0 {
+ t.Fatal("should be set")
+ }
+
+ past := GetMillis() - 1
+ o = Post{Message: "test", CreateAt: past}
+ o.PreSave()
+
+ if o.CreateAt > past {
+ t.Fatal("should not be updated")
+ }
+
o.Etag()
}
diff --git a/model/session.go b/model/session.go
index 3c7c75eb4..bb4d987a7 100644
--- a/model/session.go
+++ b/model/session.go
@@ -10,6 +10,7 @@ import (
const (
SESSION_TOKEN = "MMSID"
+ MULTI_SESSION_TOKEN = "MMSIDMU"
SESSION_TIME_WEB_IN_DAYS = 30
SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS
SESSION_TIME_MOBILE_IN_DAYS = 30
diff --git a/model/utils.go b/model/utils.go
index e19cceba5..93b8c4512 100644
--- a/model/utils.go
+++ b/model/utils.go
@@ -163,6 +163,7 @@ var reservedName = []string{
"post",
"cluster",
"api",
+ "oauth",
}
var wwwStart = regexp.MustCompile(`^www`)
diff --git a/model/version.go b/model/version.go
index 233fc3747..efa1697db 100644
--- a/model/version.go
+++ b/model/version.go
@@ -67,6 +67,10 @@ func GetPreviousVersion(currentVersion string) (int64, int64) {
return 0, 0
}
+func IsOfficalBuild() bool {
+ return BuildNumber != "_BUILD_NUMBER_"
+}
+
func IsCurrentVersion(versionToCheck string) bool {
currentMajor, currentMinor, _ := SplitVersion(CurrentVersion)
toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck)
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index 877246fc3..3bbe7e716 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -4,6 +4,7 @@
package store
import (
+ l4g "code.google.com/p/log4go"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -30,13 +31,55 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
tablem.ColMap("ChannelId").SetMaxSize(26)
tablem.ColMap("UserId").SetMaxSize(26)
tablem.ColMap("Roles").SetMaxSize(64)
- tablem.ColMap("NotifyLevel").SetMaxSize(20)
+ tablem.ColMap("NotifyProps").SetMaxSize(2000)
}
return s
}
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
+ if s.CreateColumnIfNotExists("ChannelMembers", "NotifyProps", "varchar(2000)", "varchar(2000)", "{}") {
+ // populate NotifyProps from existing NotifyLevel field
+
+ // set default values
+ _, err := s.GetMaster().Exec(
+ `UPDATE
+ ChannelMembers
+ SET
+ NotifyProps = CONCAT('{"desktop":"', CONCAT(NotifyLevel, '","mark_unread":"` + model.CHANNEL_MARK_UNREAD_ALL + `"}'))`)
+ if err != nil {
+ l4g.Error("Unable to set default values for ChannelMembers.NotifyProps")
+ l4g.Error(err.Error())
+ }
+
+ // assume channels with all notifications enabled are just using the default settings
+ _, err = s.GetMaster().Exec(
+ `UPDATE
+ ChannelMembers
+ SET
+ NotifyProps = '{"desktop":"` + model.CHANNEL_NOTIFY_DEFAULT + `","mark_unread":"` + model.CHANNEL_MARK_UNREAD_ALL + `"}'
+ WHERE
+ NotifyLevel = '` + model.CHANNEL_NOTIFY_ALL + `'`)
+ if err != nil {
+ l4g.Error("Unable to set values for ChannelMembers.NotifyProps when members previously had notifyLevel=all")
+ l4g.Error(err.Error())
+ }
+
+ // set quiet mode channels to have no notifications and only mark the channel unread on mentions
+ _, err = s.GetMaster().Exec(
+ `UPDATE
+ ChannelMembers
+ SET
+ NotifyProps = '{"desktop":"` + model.CHANNEL_NOTIFY_NONE + `","mark_unread":"` + model.CHANNEL_MARK_UNREAD_MENTION + `"}'
+ WHERE
+ NotifyLevel = 'quiet'`)
+ if err != nil {
+ l4g.Error("Unable to set values for ChannelMembers.NotifyProps when members previously had notifyLevel=quiet")
+ l4g.Error(err.Error())
+ }
+
+ s.RemoveColumnIfExists("ChannelMembers", "NotifyLevel")
+ }
}
func (s SqlChannelStore) CreateIndexesIfNotExists() {
@@ -386,6 +429,34 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
return storeChannel
}
+func (s SqlChannelStore) UpdateMember(member *model.ChannelMember) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ member.PreUpdate()
+
+ if result.Err = member.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if _, err := s.GetMaster().Update(member); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.UpdateMember", "We encounted an error updating the channel member",
+ "channel_id="+member.ChannelId+", "+"user_id="+member.UserId+", "+err.Error())
+ } else {
+ result.Data = member
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlChannelStore) GetMembers(channelId string) StoreChannel {
storeChannel := make(StoreChannel)
@@ -583,7 +654,7 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelId string, userId string) Sto
var query string
- if utils.Cfg.SqlSettings.DriverName == "postgres" {
+ if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
query = `UPDATE
ChannelMembers
SET
@@ -597,7 +668,7 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelId string, userId string) Sto
Channels.Id = ChannelMembers.ChannelId
AND UserId = :UserId
AND ChannelId = :ChannelId`
- } else if utils.Cfg.SqlSettings.DriverName == "mysql" {
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
query = `UPDATE
ChannelMembers, Channels
SET
@@ -649,35 +720,6 @@ func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string)
return storeChannel
}
-func (s SqlChannelStore) UpdateNotifyLevel(channelId, userId, notifyLevel string) StoreChannel {
- storeChannel := make(StoreChannel)
-
- go func() {
- result := StoreResult{}
-
- updateAt := model.GetMillis()
-
- _, err := s.GetMaster().Exec(
- `UPDATE
- ChannelMembers
- SET
- NotifyLevel = :NotifyLevel,
- LastUpdateAt = :LastUpdateAt
- WHERE
- UserId = :UserId
- AND ChannelId = :ChannelId`,
- map[string]interface{}{"ChannelId": channelId, "UserId": userId, "NotifyLevel": notifyLevel, "LastUpdateAt": updateAt})
- if err != nil {
- result.Err = model.NewAppError("SqlChannelStore.UpdateNotifyLevel", "We couldn't update the notify level", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
- }
-
- storeChannel <- result
- close(storeChannel)
- }()
-
- return storeChannel
-}
-
func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go
index dabe39904..b6d05684b 100644
--- a/store/sql_channel_store_test.go
+++ b/store/sql_channel_store_test.go
@@ -135,13 +135,13 @@ func TestChannelStoreDelete(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
- m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
if r := <-store.Channel().Delete(o1.Id, model.GetMillis()); r.Err != nil {
@@ -222,13 +222,13 @@ func TestChannelMemberStore(t *testing.T) {
o1 := model.ChannelMember{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
- o1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ o1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&o1))
o2 := model.ChannelMember{}
o2.ChannelId = c1.Id
o2.UserId = u2.Id
- o2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ o2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&o2))
c1t2 := (<-store.Channel().Get(c1.Id)).Data.(*model.Channel)
@@ -291,7 +291,7 @@ func TestChannelStorePermissionsTo(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
count := (<-store.Channel().CheckPermissionsTo(o1.TeamId, o1.Id, m1.UserId)).Data.(int64)
@@ -371,19 +371,19 @@ func TestChannelStoreGetChannels(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
- m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
- m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3))
cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId)
@@ -414,19 +414,19 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
- m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
- m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3))
o3 := model.Channel{}
@@ -482,19 +482,19 @@ func TestChannelStoreGetChannelCounts(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
- m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m2))
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
- m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m3))
cresult := <-store.Channel().GetChannelCounts(o1.TeamId, m1.UserId)
@@ -523,7 +523,7 @@ func TestChannelStoreUpdateLastViewedAt(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
err := (<-store.Channel().UpdateLastViewedAt(m1.ChannelId, m1.UserId)).Err
@@ -551,7 +551,7 @@ func TestChannelStoreIncrementMentionCount(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
err := (<-store.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId)).Err
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index 21e8e9d00..668f45fbb 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -418,7 +418,7 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
var posts []*model.Post
- if utils.Cfg.SqlSettings.DriverName == "postgres" {
+ if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
// Parse text for wildcards
if wildcard, err := regexp.Compile("\\*($| )"); err == nil {
@@ -451,7 +451,7 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error())
}
- } else if utils.Cfg.SqlSettings.DriverName == "mysql" {
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
searchQuery := fmt.Sprintf(`SELECT
*
FROM
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index bc1cb2c2c..6a6364dc8 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -484,7 +484,7 @@ func TestPostStoreSearch(t *testing.T) {
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = userId
- m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
c2 := &model.Channel{}
@@ -530,13 +530,6 @@ func TestPostStoreSearch(t *testing.T) {
t.Fatal("returned wrong serach result")
}
- // if utils.Cfg.SqlSettings.DriverName == "mysql" {
- // r2 := (<-store.Post().Search(teamId, userId, "new york", false)).Data.(*model.PostList)
- // if len(r2.Order) >= 1 && r2.Order[0] != o2.Id {
- // t.Fatal("returned wrong serach result")
- // }
- // }
-
r3 := (<-store.Post().Search(teamId, userId, "new", false)).Data.(*model.PostList)
if len(r3.Order) != 2 && r3.Order[0] != o1.Id {
t.Fatal("returned wrong serach result")
diff --git a/store/sql_store.go b/store/sql_store.go
index 6dcf2e8cd..3ef9cfbc4 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -160,9 +160,9 @@ func setupConnection(con_type string, driver string, dataSource string, maxIdle
if driver == "sqlite3" {
dbmap = &gorp.DbMap{Db: db, TypeConverter: mattermConverter{}, Dialect: gorp.SqliteDialect{}}
- } else if driver == "mysql" {
+ } else if driver == model.DATABASE_DRIVER_MYSQL {
dbmap = &gorp.DbMap{Db: db, TypeConverter: mattermConverter{}, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8MB4"}}
- } else if driver == "postgres" {
+ } else if driver == model.DATABASE_DRIVER_POSTGRES {
dbmap = &gorp.DbMap{Db: db, TypeConverter: mattermConverter{}, Dialect: gorp.PostgresDialect{}}
} else {
l4g.Critical("Failed to create dialect specific driver")
@@ -183,7 +183,7 @@ func (ss SqlStore) GetCurrentSchemaVersion() string {
}
func (ss SqlStore) DoesTableExist(tableName string) bool {
- if utils.Cfg.SqlSettings.DriverName == "postgres" {
+ if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
count, err := ss.GetMaster().SelectInt(
`SELECT count(relname) FROM pg_class WHERE relname=$1`,
strings.ToLower(tableName),
@@ -197,7 +197,7 @@ func (ss SqlStore) DoesTableExist(tableName string) bool {
return count > 0
- } else if utils.Cfg.SqlSettings.DriverName == "mysql" {
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
count, err := ss.GetMaster().SelectInt(
`SELECT
@@ -228,7 +228,7 @@ func (ss SqlStore) DoesTableExist(tableName string) bool {
}
func (ss SqlStore) DoesColumnExist(tableName string, columnName string) bool {
- if utils.Cfg.SqlSettings.DriverName == "postgres" {
+ if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
count, err := ss.GetMaster().SelectInt(
`SELECT COUNT(0)
FROM pg_attribute
@@ -251,7 +251,7 @@ func (ss SqlStore) DoesColumnExist(tableName string, columnName string) bool {
return count > 0
- } else if utils.Cfg.SqlSettings.DriverName == "mysql" {
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
count, err := ss.GetMaster().SelectInt(
`SELECT
@@ -288,7 +288,7 @@ func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string,
return false
}
- if utils.Cfg.SqlSettings.DriverName == "postgres" {
+ if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
_, err := ss.GetMaster().Exec("ALTER TABLE " + tableName + " ADD " + columnName + " " + postgresColType + " DEFAULT '" + defaultValue + "'")
if err != nil {
l4g.Critical("Failed to create column %v", err)
@@ -298,7 +298,7 @@ func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string,
return true
- } else if utils.Cfg.SqlSettings.DriverName == "mysql" {
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
_, err := ss.GetMaster().Exec("ALTER TABLE " + tableName + " ADD " + columnName + " " + mySqlColType + " DEFAULT '" + defaultValue + "'")
if err != nil {
l4g.Critical("Failed to create column %v", err)
@@ -334,7 +334,7 @@ func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) boo
// func (ss SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool {
// // XXX TODO FIXME this should be removed after 0.6.0
-// if utils.Cfg.SqlSettings.DriverName == "postgres" {
+// if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
// return false
// }
@@ -363,7 +363,7 @@ func (ss SqlStore) CreateFullTextIndexIfNotExists(indexName string, tableName st
func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, columnName string, fullText bool) {
- if utils.Cfg.SqlSettings.DriverName == "postgres" {
+ if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
_, err := ss.GetMaster().SelectStr("SELECT $1::regclass", indexName)
// It should fail if the index does not exist
if err == nil {
@@ -383,7 +383,7 @@ func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, co
time.Sleep(time.Second)
panic("Failed to create index " + err.Error())
}
- } else if utils.Cfg.SqlSettings.DriverName == "mysql" {
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
count, err := ss.GetMaster().SelectInt("SELECT COUNT(0) AS index_exists FROM information_schema.statistics WHERE TABLE_SCHEMA = DATABASE() and table_name = ? AND index_name = ?", tableName, indexName)
if err != nil {
diff --git a/store/store.go b/store/store.go
index 23580f452..887913bc6 100644
--- a/store/store.go
+++ b/store/store.go
@@ -62,6 +62,7 @@ type ChannelStore interface {
GetForExport(teamId string) StoreChannel
SaveMember(member *model.ChannelMember) StoreChannel
+ UpdateMember(member *model.ChannelMember) StoreChannel
GetMembers(channelId string) StoreChannel
GetMember(channelId string, userId string) StoreChannel
RemoveMember(channelId string, userId string) StoreChannel
@@ -71,7 +72,6 @@ type ChannelStore interface {
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
UpdateLastViewedAt(channelId string, userId string) StoreChannel
IncrementMentionCount(channelId string, userId string) StoreChannel
- UpdateNotifyLevel(channelId string, userId string, notifyLevel string) StoreChannel
}
type PostStore interface {
diff --git a/utils/config.go b/utils/config.go
index 5d786699b..44c4c43af 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -150,7 +150,12 @@ func LoadConfig(fileName string) {
CfgFileName = fileName
}
+ if err := config.IsValid(); err != nil {
+ panic("Error validating config file=" + fileName + ", err=" + err.Message)
+ }
+
configureLog(&config.LogSettings)
+ TestConnection(&config)
Cfg = &config
SanitizeOptions = getSanitizeOptions(Cfg)
@@ -179,6 +184,8 @@ func getClientProperties(c *model.Config) map[string]string {
props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey
props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks)
+ props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride)
+ props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride)
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
diff --git a/utils/diagnostic.go b/utils/diagnostic.go
new file mode 100644
index 000000000..9a61ae934
--- /dev/null
+++ b/utils/diagnostic.go
@@ -0,0 +1,45 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "net/http"
+
+ l4g "code.google.com/p/log4go"
+
+ "github.com/mattermost/platform/model"
+)
+
+const (
+ PROP_DIAGNOSTIC_ID = "id"
+ PROP_DIAGNOSTIC_CATEGORY = "c"
+ VAL_DIAGNOSTIC_CATEGORY_DEFALUT = "d"
+ PROP_DIAGNOSTIC_BUILD = "b"
+ PROP_DIAGNOSTIC_DATABASE = "db"
+ PROP_DIAGNOSTIC_OS = "os"
+ PROP_DIAGNOSTIC_USER_COUNT = "uc"
+)
+
+func SendDiagnostic(data model.StringMap) *model.AppError {
+ if Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() {
+
+ query := "?"
+ for name, value := range data {
+ if len(query) > 1 {
+ query += "&"
+ }
+
+ query += name + "=" + UrlEncode(value)
+ }
+
+ res, err := http.Get("http://d7zmvsa9e04kk.cloudfront.net/i" + query)
+ if err != nil {
+ l4g.Error("Failed to send diagnostics %v", err.Error())
+ }
+
+ res.Body.Close()
+ }
+
+ return nil
+}
diff --git a/utils/mail.go b/utils/mail.go
index dd975155d..f6fe1ce00 100644
--- a/utils/mail.go
+++ b/utils/mail.go
@@ -65,13 +65,34 @@ func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.Ap
return c, nil
}
+func TestConnection(config *model.Config) {
+ if !config.EmailSettings.SendEmailNotifications {
+ return
+ }
+
+ conn, err1 := connectToSMTPServer(config)
+ if err1 != nil {
+ l4g.Error("SMTP server settings do not appear to be configured properly err=%v details=%v", err1.Message, err1.DetailedError)
+ return
+ }
+ defer conn.Close()
+
+ c, err2 := newSMTPClient(conn, config)
+ if err2 != nil {
+ l4g.Error("SMTP connection settings do not appear to be configured properly err=%v details=%v", err2.Message, err2.DetailedError)
+ return
+ }
+ defer c.Quit()
+ defer c.Close()
+}
+
func SendMail(to, subject, body string) *model.AppError {
return SendMailUsingConfig(to, subject, body, Cfg)
}
func SendMailUsingConfig(to, subject, body string, config *model.Config) *model.AppError {
- if !config.EmailSettings.SendEmailNotifications {
+ if !config.EmailSettings.SendEmailNotifications || len(config.EmailSettings.SMTPServer) == 0 {
return nil
}
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index 3b5ad2a1a..762a4ab26 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -288,7 +288,7 @@ export default class EmailSettings extends React.Component {
className='control-label col-sm-4'
htmlFor='feedbackName'
>
- {'Feedback Name:'}
+ {'Notification Display Name:'}
</label>
<div className='col-sm-8'>
<input
@@ -310,7 +310,7 @@ export default class EmailSettings extends React.Component {
className='control-label col-sm-4'
htmlFor='feedbackEmail'
>
- {'Feedback Email:'}
+ {'Notification Email Address:'}
</label>
<div className='col-sm-8'>
<input
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
index eaea15b8f..759892ad3 100644
--- a/web/react/components/admin_console/gitlab_settings.jsx
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -40,7 +40,6 @@ export default class GitLabSettings extends React.Component {
config.GitLabSettings.Enable = React.findDOMNode(this.refs.Enable).checked;
config.GitLabSettings.Secret = React.findDOMNode(this.refs.Secret).value.trim();
config.GitLabSettings.Id = React.findDOMNode(this.refs.Id).value.trim();
- config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim();
config.GitLabSettings.AuthEndpoint = React.findDOMNode(this.refs.AuthEndpoint).value.trim();
config.GitLabSettings.TokenEndpoint = React.findDOMNode(this.refs.TokenEndpoint).value.trim();
config.GitLabSettings.UserApiEndpoint = React.findDOMNode(this.refs.UserApiEndpoint).value.trim();
@@ -121,28 +120,6 @@ export default class GitLabSettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='Secret'
- >
- {'Secret:'}
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Secret'
- ref='Secret'
- placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- defaultValue={this.props.config.GitLabSettings.Secret}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab.'}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
htmlFor='Id'
>
{'Id:'}
@@ -165,22 +142,22 @@ export default class GitLabSettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='Scope'
+ htmlFor='Secret'
>
- {'Scope:'}
+ {'Secret:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
- id='Scope'
- ref='Scope'
- placeholder='Not currently used by GitLab. Please leave blank'
- defaultValue={this.props.config.GitLabSettings.Scope}
+ id='Secret'
+ ref='Secret'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.GitLabSettings.Secret}
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
+ <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab.'}</p>
</div>
</div>
@@ -202,7 +179,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize).'}</p>
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
</div>
</div>
@@ -224,7 +201,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token.'}</p>
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
</div>
</div>
@@ -246,7 +223,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user.'}</p>
+ <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
</div>
</div>
@@ -272,6 +249,30 @@ export default class GitLabSettings extends React.Component {
}
}
+
+//config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim();
+// <div className='form-group'>
+// <label
+// className='control-label col-sm-4'
+// htmlFor='Scope'
+// >
+// {'Scope:'}
+// </label>
+// <div className='col-sm-8'>
+// <input
+// type='text'
+// className='form-control'
+// id='Scope'
+// ref='Scope'
+// placeholder='Not currently used by GitLab. Please leave blank'
+// defaultValue={this.props.config.GitLabSettings.Scope}
+// onChange={this.handleChange}
+// disabled={!this.state.Allow}
+// />
+// <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
+// </div>
+// </div>
+
GitLabSettings.propTypes = {
config: React.PropTypes.object
};
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
index affd8ae11..c74d321e6 100644
--- a/web/react/components/admin_console/privacy_settings.jsx
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -30,6 +30,7 @@ export default class PrivacySettings extends React.Component {
var config = this.props.config;
config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
+ config.PrivacySettings.EnableDiagnostic = React.findDOMNode(this.refs.EnableDiagnostic).checked;
Client.saveConfig(
config,
@@ -137,6 +138,39 @@ export default class PrivacySettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableDiagnostic'
+ >
+ {'Send Error and Diagnostic: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableDiagnostic'
+ value='true'
+ ref='EnableDiagnostic'
+ defaultChecked={this.props.config.PrivacySettings.EnableDiagnostic}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableDiagnostic'
+ value='false'
+ defaultChecked={!this.props.config.PrivacySettings.EnableDiagnostic}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, The server will periodically send error and diagnostic information to Mattermost.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 245ffa871..b2d1b7b4d 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -37,6 +37,8 @@ export default class ServiceSettings extends React.Component {
config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
//config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
+ config.ServiceSettings.EnablePostUsernameOverride = React.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
+ config.ServiceSettings.EnablePostIconOverride = React.findDOMNode(this.refs.EnablePostIconOverride).checked;
config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
var MaximumLoginAttempts = 10;
@@ -199,7 +201,73 @@ export default class ServiceSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When true, incoming webhooks will be allowed.'}</p>
+ <p className='help-text'>{'When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnablePostUsernameOverride'
+ >
+ {'Enable Overriding Usernames from Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostUsernameOverride'
+ value='true'
+ ref='EnablePostUsernameOverride'
+ defaultChecked={this.props.config.ServiceSettings.EnablePostUsernameOverride}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostUsernameOverride'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnablePostUsernameOverride}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnablePostIconOverride'
+ >
+ {'Enable Overriding Icon from Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostIconOverride'
+ value='true'
+ ref='EnablePostIconOverride'
+ defaultChecked={this.props.config.ServiceSettings.EnablePostIconOverride}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostIconOverride'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnablePostIconOverride}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx
index 430a7453b..0e0ceb9af 100644
--- a/web/react/components/admin_console/sql_settings.jsx
+++ b/web/react/components/admin_console/sql_settings.jsx
@@ -73,6 +73,12 @@ export default class SqlSettings extends React.Component {
handleGenerate(e) {
e.preventDefault();
+
+ var cfm = global.window.confirm('Warning: re-generating this salt may cause some columns in the database to return empty results.');
+ if (cfm === false) {
+ return;
+ }
+
React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 9eda68b38..45981b295 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -15,14 +15,24 @@ export default class ChannelNotifications extends React.Component {
this.onListenerChange = this.onListenerChange.bind(this);
this.updateSection = this.updateSection.bind(this);
- this.handleUpdate = this.handleUpdate.bind(this);
- this.handleRadioClick = this.handleRadioClick.bind(this);
- this.handleQuietToggle = this.handleQuietToggle.bind(this);
- this.createDesktopSection = this.createDesktopSection.bind(this);
- this.createQuietSection = this.createQuietSection.bind(this);
- this.state = {notifyLevel: '', title: '', channelId: '', activeSection: ''};
+ this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
+ this.handleUpdateNotifyLevel = this.handleUpdateNotifyLevel.bind(this);
+ this.createNotifyLevelSection = this.createNotifyLevelSection.bind(this);
+
+ this.handleSubmitMarkUnreadLevel = this.handleSubmitMarkUnreadLevel.bind(this);
+ this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
+ this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
+
+ this.state = {
+ notifyLevel: '',
+ markUnreadLevel: '',
+ title: '',
+ channelId: '',
+ activeSection: ''
+ };
}
+
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
@@ -30,33 +40,34 @@ export default class ChannelNotifications extends React.Component {
var button = e.relatedTarget;
var channelId = button.getAttribute('data-channelid');
- var notifyLevel = ChannelStore.getMember(channelId).notify_level;
- var quietMode = false;
-
- if (notifyLevel === 'quiet') {
- quietMode = true;
- }
+ const member = ChannelStore.getMember(channelId);
+ var notifyLevel = member.notify_props.desktop;
+ var markUnreadLevel = member.notify_props.mark_unread;
- this.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId});
+ this.setState({
+ notifyLevel,
+ markUnreadLevel,
+ title: button.getAttribute('data-title'),
+ channelId: channelId
+ });
}.bind(this));
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
}
+
onListenerChange() {
if (!this.state.channelId) {
return;
}
- var notifyLevel = ChannelStore.getMember(this.state.channelId).notify_level;
- var quietMode = false;
- if (notifyLevel === 'quiet') {
- quietMode = true;
- }
+ const member = ChannelStore.getMember(this.state.channelId);
+ var notifyLevel = member.notify_props.desktop;
+ var markUnreadLevel = member.notify_props.mark_unread;
var newState = this.state;
newState.notifyLevel = notifyLevel;
- newState.quietMode = quietMode;
+ newState.markUnreadLevel = markUnreadLevel;
if (!Utils.areStatesEqual(this.state, newState)) {
this.setState(newState);
@@ -65,53 +76,64 @@ export default class ChannelNotifications extends React.Component {
updateSection(section) {
this.setState({activeSection: section});
}
- handleUpdate() {
+
+ handleSubmitNotifyLevel() {
var channelId = this.state.channelId;
var notifyLevel = this.state.notifyLevel;
- if (this.state.quietMode) {
- notifyLevel = 'quiet';
+
+ if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) {
+ this.updateSection('');
+ return;
}
var data = {};
data.channel_id = channelId;
data.user_id = UserStore.getCurrentId();
- data.notify_level = notifyLevel;
-
- if (!data.notify_level || data.notify_level.length === 0) {
- return;
- }
+ data.desktop = notifyLevel;
- Client.updateNotifyLevel(data,
- function success() {
+ Client.updateNotifyProps(data,
+ () => {
var member = ChannelStore.getMember(channelId);
- member.notify_level = notifyLevel;
+ member.notify_props.desktop = notifyLevel;
ChannelStore.setChannelMember(member);
this.updateSection('');
- }.bind(this),
- function error(err) {
+ },
+ (err) => {
this.setState({serverError: err.message});
- }.bind(this)
+ }
);
}
- handleRadioClick(notifyLevel) {
- this.setState({notifyLevel: notifyLevel, quietMode: false});
- React.findDOMNode(this.refs.modal).focus();
- }
- handleQuietToggle(quietMode) {
- this.setState({notifyLevel: 'none', quietMode: quietMode});
+
+ handleUpdateNotifyLevel(notifyLevel) {
+ this.setState({notifyLevel});
React.findDOMNode(this.refs.modal).focus();
}
- createDesktopSection(serverError) {
+
+ createNotifyLevelSection(serverError) {
var handleUpdateSection;
+ const user = UserStore.getCurrentUser();
+ const globalNotifyLevel = user.notify_props.desktop;
+
+ let globalNotifyLevelName;
+ if (globalNotifyLevel === 'all') {
+ globalNotifyLevelName = 'For all activity';
+ } else if (globalNotifyLevel === 'mention') {
+ globalNotifyLevelName = 'Only for mentions';
+ } else {
+ globalNotifyLevelName = 'Never';
+ }
+
if (this.state.activeSection === 'desktop') {
- var notifyActive = [false, false, false];
- if (this.state.notifyLevel === 'mention') {
- notifyActive[1] = true;
- } else if (this.state.notifyLevel === 'all') {
+ var notifyActive = [false, false, false, false];
+ if (this.state.notifyLevel === 'default') {
notifyActive[0] = true;
- } else {
+ } else if (this.state.notifyLevel === 'all') {
+ notifyActive[1] = true;
+ } else if (this.state.notifyLevel === 'mention') {
notifyActive[2] = true;
+ } else {
+ notifyActive[3] = true;
}
var inputs = [];
@@ -123,9 +145,9 @@ export default class ChannelNotifications extends React.Component {
<input
type='radio'
checked={notifyActive[0]}
- onChange={this.handleRadioClick.bind(this, 'all')}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
>
- For all activity
+ {`Global default (${globalNotifyLevelName})`}
</input>
</label>
<br/>
@@ -135,9 +157,9 @@ export default class ChannelNotifications extends React.Component {
<input
type='radio'
checked={notifyActive[1]}
- onChange={this.handleRadioClick.bind(this, 'mention')}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
>
- Only for mentions
+ {'For all activity'}
</input>
</label>
<br/>
@@ -147,9 +169,21 @@ export default class ChannelNotifications extends React.Component {
<input
type='radio'
checked={notifyActive[2]}
- onChange={this.handleRadioClick.bind(this, 'none')}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
+ >
+ {'Only for mentions'}
+ </input>
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={notifyActive[3]}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
>
- Never
+ {'Never'}
</input>
</label>
</div>
@@ -162,30 +196,19 @@ export default class ChannelNotifications extends React.Component {
e.preventDefault();
}.bind(this);
- let curChannel = ChannelStore.get(this.state.channelId);
- let extraInfo = (
+ const extraInfo = (
<span>
- These settings will override the global notification settings.
+ {'Selecting an option other than "Default" will override the global notification settings.'}
<br/>
- Desktop notifications are available on Firefox, Safari, and Chrome.
+ {'Desktop notifications are available on Firefox, Safari, and Chrome.'}
</span>
);
- if (curChannel && curChannel.display_name) {
- extraInfo = (
- <span>
- These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel.
- <br/>
- Desktop notifications are available on Firefox, Safari, and Chrome.
- </span>
- );
- }
-
return (
<SettingItemMax
title='Send desktop notifications'
inputs={inputs}
- submit={this.handleUpdate}
+ submit={this.handleSubmitNotifyLevel}
server_error={serverError}
updateSection={handleUpdateSection}
extraInfo={extraInfo}
@@ -194,7 +217,9 @@ export default class ChannelNotifications extends React.Component {
}
var describe;
- if (this.state.notifyLevel === 'mention') {
+ if (this.state.notifyLevel === 'default') {
+ describe = `Global default (${globalNotifyLevelName})`;
+ } else if (this.state.notifyLevel === 'mention') {
describe = 'Only for mentions';
} else if (this.state.notifyLevel === 'all') {
describe = 'For all activity';
@@ -215,101 +240,123 @@ export default class ChannelNotifications extends React.Component {
/>
);
}
- createQuietSection(serverError) {
- var handleUpdateSection;
- if (this.state.activeSection === 'quiet') {
- var quietActive = [false, false];
- if (this.state.quietMode) {
- quietActive[0] = true;
- } else {
- quietActive[1] = true;
+
+ handleSubmitMarkUnreadLevel() {
+ const channelId = this.state.channelId;
+ const markUnreadLevel = this.state.markUnreadLevel;
+
+ if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) {
+ this.updateSection('');
+ return;
+ }
+
+ const data = {
+ channel_id: channelId,
+ user_id: UserStore.getCurrentId(),
+ mark_unread: markUnreadLevel
+ };
+
+ Client.updateNotifyProps(data,
+ () => {
+ var member = ChannelStore.getMember(channelId);
+ member.notify_props.mark_unread = markUnreadLevel;
+ ChannelStore.setChannelMember(member);
+ this.updateSection('');
+ },
+ (err) => {
+ this.setState({serverError: err.message});
}
+ );
+ }
- var inputs = [];
+ handleUpdateMarkUnreadLevel(markUnreadLevel) {
+ this.setState({markUnreadLevel});
+ React.findDOMNode(this.refs.modal).focus();
+ }
- inputs.push(
+ createMarkUnreadLevelSection(serverError) {
+ let content;
+
+ if (this.state.activeSection === 'markUnreadLevel') {
+ const inputs = [(
<div>
<div className='radio'>
<label>
<input
type='radio'
- checked={quietActive[0]}
- onChange={this.handleQuietToggle.bind(this, true)}
+ checked={this.state.markUnreadLevel === 'all'}
+ onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
>
- On
+ {'For all unread messages'}
</input>
</label>
- <br/>
+ <br />
</div>
<div className='radio'>
<label>
<input
type='radio'
- checked={quietActive[1]}
- onChange={this.handleQuietToggle.bind(this, false)}
+ checked={this.state.markUnreadLevel === 'mention'}
+ onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
>
- Off
+ {'Only for mentions'}
</input>
</label>
- <br/>
+ <br />
</div>
</div>
- );
-
- inputs.push(
- <div>
- <br/>
- Enabling quiet mode will turn off desktop notifications and only mark the channel as unread if you have been mentioned.
- </div>
- );
+ )];
- handleUpdateSection = function updateSection(e) {
+ const handleUpdateSection = function handleUpdateSection(e) {
this.updateSection('');
this.onListenerChange();
e.preventDefault();
}.bind(this);
- return (
+ const extraInfo = <span>{'The channel name is bolded in the sidebar when there are unread messages. Selecting "Only for mentions" will bold the channel only when you are mentioned.'}</span>;
+
+ content = (
<SettingItemMax
- title='Quiet mode'
+ title='Mark Channel Unread'
inputs={inputs}
- submit={this.handleUpdate}
+ submit={this.handleSubmitMarkUnreadLevel}
server_error={serverError}
updateSection={handleUpdateSection}
+ extraInfo={extraInfo}
/>
);
- }
-
- var describe;
- if (this.state.quietMode) {
- describe = 'On';
} else {
- describe = 'Off';
- }
+ let describe;
- handleUpdateSection = function updateSection(e) {
- this.updateSection('quiet');
- e.preventDefault();
- }.bind(this);
+ if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
+ describe = 'For all unread messages';
+ } else {
+ describe = 'Only for mentions';
+ }
- return (
- <SettingItemMin
- title='Quiet mode'
- describe={describe}
- updateSection={handleUpdateSection}
- />
- );
+ const handleUpdateSection = function handleUpdateSection(e) {
+ this.updateSection('markUnreadLevel');
+ e.preventDefault();
+ }.bind(this);
+
+ content = (
+ <SettingItemMin
+ title='Mark Channel Unread'
+ describe={describe}
+ updateSection={handleUpdateSection}
+ />
+ );
+ }
+
+ return content;
}
+
render() {
var serverError = null;
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
- var desktopSection = this.createDesktopSection(serverError);
-
- var quietSection = this.createQuietSection(serverError);
-
return (
<div
className='modal fade'
@@ -341,9 +388,9 @@ export default class ChannelNotifications extends React.Component {
>
<br/>
<div className='divider-dark first'/>
- {desktopSection}
+ {this.createNotifyLevelSection(serverError)}
<div className='divider-light'/>
- {quietSection}
+ {this.createMarkUnreadLevelSection(serverError)}
<div className='divider-dark'/>
</div>
</div>
diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx
index 63bd57c2a..fea7085b7 100644
--- a/web/react/components/command_list.jsx
+++ b/web/react/components/command_list.jsx
@@ -96,4 +96,4 @@ CommandList.defaultProps = {
CommandList.propTypes = {
addCommand: React.PropTypes.func,
channelId: React.PropTypes.string
-}; \ No newline at end of file
+};
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index b7bce9b34..54d77c358 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -43,7 +43,7 @@ export default class MoreDirectChannels extends React.Component {
handleClick = function clickHandler(e) {
e.preventDefault();
- utils.switchChannel(channel, channel.teammate_username);
+ utils.switchChannel(channel);
$(React.findDOMNode(self.refs.modal)).modal('hide');
};
} else {
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 4c01d2c43..78057d10b 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -9,7 +9,7 @@ var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
function getStateFromStores() {
- return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()};
+ return {teams: UserStore.getTeams()};
}
export default class NavbarDropdown extends React.Component {
@@ -62,7 +62,7 @@ export default class NavbarDropdown extends React.Component {
if (currentUser != null) {
isAdmin = Utils.isAdmin(currentUser.roles);
- isSystemAdmin = Utils.isInRole(currentUser.roles, 'system_admin');
+ isSystemAdmin = Utils.isSystemAdmin(currentUser.roles);
inviteLink = (
<li>
@@ -142,10 +142,10 @@ export default class NavbarDropdown extends React.Component {
>
</li>
);
- if (this.state.teams.length > 1 && this.state.currentTeam) {
- var curTeamName = this.state.currentTeam.name;
+
+ if (this.state.teams.length > 1) {
this.state.teams.forEach((teamName) => {
- if (teamName !== curTeamName) {
+ if (teamName !== this.props.teamName) {
teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>);
}
});
@@ -234,5 +234,7 @@ NavbarDropdown.defaultProps = {
teamType: ''
};
NavbarDropdown.propTypes = {
- teamType: React.PropTypes.string
+ teamType: React.PropTypes.string,
+ teamDisplayName: React.PropTypes.string,
+ teamName: React.PropTypes.string
};
diff --git a/web/react/components/notify_counts.jsx b/web/react/components/notify_counts.jsx
index 0b7c41b62..f34b4669f 100644
--- a/web/react/components/notify_counts.jsx
+++ b/web/react/components/notify_counts.jsx
@@ -15,7 +15,7 @@ function getCountsStateFromStores() {
count += channel.total_msg_count - channelMember.msg_count;
} else if (channelMember.mention_count > 0) {
count += channelMember.mention_count;
- } else if (channelMember.notify_level !== 'quiet' && channel.total_msg_count - channelMember.msg_count > 0) {
+ } else if (channelMember.notify_props.mark_unread !== 'mention' && channel.total_msg_count - channelMember.msg_count > 0) {
count += 1;
}
});
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index a2ca8b00f..95a88c3d6 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+var UserStore = require('../stores/user_store.jsx');
+
export default class PopoverListMembers extends React.Component {
componentDidMount() {
const originalLeave = $.fn.popover.Constructor.prototype.leave;
@@ -32,22 +34,28 @@ export default class PopoverListMembers extends React.Component {
}
render() {
let popoverHtml = '';
+ let count = 0;
+ let countText = '-';
const members = this.props.members;
- let count;
- if (members.length > 20) {
- count = '20+';
- } else {
- count = members.length || '-';
- }
+ const teamMembers = UserStore.getProfilesUsernameMap();
- if (members) {
+ if (members && teamMembers) {
members.sort(function compareByLocal(a, b) {
return a.username.localeCompare(b.username);
});
members.forEach(function addMemberElement(m) {
- popoverHtml += `<div class='text--nowrap'>${m.username}</div>`;
+ if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
+ popoverHtml += `<div class='text--nowrap'>${m.username}</div>`;
+ count++;
+ }
});
+
+ if (count > 20) {
+ countText = '20+';
+ } else if (count > 0) {
+ countText = count.toString();
+ }
}
return (
@@ -63,7 +71,7 @@ export default class PopoverListMembers extends React.Component {
data-toggle='tooltip'
title='View Channel Members'
>
- {count}
+ {countText}
<span
className='fa fa-user'
aria-hidden='true'
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 9127f00de..ac9c9252e 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -158,11 +158,18 @@ export default class Post extends React.Component {
var profilePic = null;
if (!this.props.hideProfilePic) {
+ let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
+ if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') {
+ if (post.props.override_icon_url) {
+ src = post.props.override_icon_url;
+ }
+ }
+
profilePic = (
<div className='post-profile-img__container'>
<img
className='post-profile-img'
- src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp}
+ src={src}
height='36'
width='36'
/>
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 6e98e4aba..6cfd243de 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -12,7 +12,10 @@ export default class PostBody extends React.Component {
constructor(props) {
super(props);
+ this.receivedYoutubeData = false;
+
this.parseEmojis = this.parseEmojis.bind(this);
+ this.createYoutubeEmbed = this.createYoutubeEmbed.bind(this);
const linkData = Utils.extractLinks(this.props.post.message);
this.state = {links: linkData.links, message: linkData.text};
@@ -50,6 +53,123 @@ export default class PostBody extends React.Component {
this.setState({links: linkData.links, message: linkData.text});
}
+ handleYoutubeTime(link) {
+ const timeRegex = /[\\?&]t=([0-9hms]+)/;
+
+ const time = link.trim().match(timeRegex);
+ if (!time || !time[1]) {
+ return '';
+ }
+
+ const hours = time[1].match(/([0-9]+)h/);
+ const minutes = time[1].match(/([0-9]+)m/);
+ const seconds = time[1].match(/([0-9]+)s/);
+
+ let ticks = 0;
+
+ if (hours && hours[1]) {
+ ticks += parseInt(hours[1], 10) * 3600;
+ }
+
+ if (minutes && minutes[1]) {
+ ticks += parseInt(minutes[1], 10) * 60;
+ }
+
+ if (seconds && seconds[1]) {
+ ticks += parseInt(seconds[1], 10);
+ }
+
+ return '&start=' + ticks.toString();
+ }
+
+ createYoutubeEmbed(link) {
+ const ytRegex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
+
+ const match = link.trim().match(ytRegex);
+ if (!match || match[1].length !== 11) {
+ return null;
+ }
+
+ const youtubeId = match[1];
+ const time = this.handleYoutubeTime(link);
+
+ function onClick(e) {
+ var div = $(e.target).closest('.video-thumbnail__container')[0];
+ var iframe = document.createElement('iframe');
+ iframe.setAttribute('src',
+ 'https://www.youtube.com/embed/' +
+ div.id +
+ '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1' +
+ time);
+ iframe.setAttribute('width', '480px');
+ iframe.setAttribute('height', '360px');
+ iframe.setAttribute('type', 'text/html');
+ iframe.setAttribute('frameborder', '0');
+ iframe.setAttribute('allowfullscreen', 'allowfullscreen');
+
+ div.parentNode.replaceChild(iframe, div);
+ }
+
+ function success(data) {
+ if (!data.items.length || !data.items[0].snippet) {
+ return null;
+ }
+ var metadata = data.items[0].snippet;
+ this.receivedYoutubeData = true;
+ this.setState({youtubeUploader: metadata.channelTitle, youtubeTitle: metadata.title});
+ }
+
+ if (global.window.config.GoogleDeveloperKey && !this.receivedYoutubeData) {
+ $.ajax({
+ async: true,
+ url: 'https://www.googleapis.com/youtube/v3/videos',
+ type: 'GET',
+ data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey},
+ success: success.bind(this)
+ });
+ }
+
+ let header = 'Youtube';
+ if (this.state.youtubeTitle) {
+ header = header + ' - ';
+ }
+
+ let uploader = this.state.youtubeUploader;
+ if (!uploader) {
+ uploader = 'unknown';
+ }
+
+ return (
+ <div className='post-comment'>
+ <h4>
+ <span className='video-type'>{header}</span>
+ <span className='video-title'><a href={link}>{this.state.youtubeTitle}</a></span>
+ </h4>
+ <h4 className='video-uploader'>{uploader}</h4>
+ <div
+ className='video-div embed-responsive-item'
+ id={youtubeId}
+ onClick={onClick}
+ >
+ <div className='embed-responsive embed-responsive-4by3 video-div__placeholder'>
+ <div
+ id={youtubeId}
+ className='video-thumbnail__container'
+ >
+ <img
+ className='video-thumbnail'
+ src={'https://i.ytimg.com/vi/' + youtubeId + '/hqdefault.jpg'}
+ />
+ <div className='block'>
+ <span className='play-button'><span/></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
render() {
const post = this.props.post;
const filenames = this.props.post.filenames;
@@ -133,7 +253,7 @@ export default class PostBody extends React.Component {
let embed;
if (filenames.length === 0 && this.state.links) {
- embed = Utils.getEmbed(this.state.links[0]);
+ embed = this.createYoutubeEmbed(this.state.links[0]);
}
let fileAttachmentHolder = '';
diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx
index 9dc525e03..dd79b3e36 100644
--- a/web/react/components/post_header.jsx
+++ b/web/react/components/post_header.jsx
@@ -12,9 +12,27 @@ export default class PostHeader extends React.Component {
render() {
var post = this.props.post;
+ let userProfile = <UserProfile userId={post.user_id} />;
+ let botIndicator;
+
+ if (post.props && post.props.from_webhook) {
+ if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') {
+ userProfile = (
+ <UserProfile
+ userId={post.user_id}
+ overwriteName={post.props.override_username}
+ disablePopover={true}
+ />
+ );
+ }
+
+ botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>;
+ }
+
return (
<ul className='post-header post-header-post'>
- <li className='post-header-col post-header__name'><strong><UserProfile userId={post.user_id} /></strong></li>
+ <li className='post-header-col post-header__name'><strong>{userProfile}</strong></li>
+ {botIndicator}
<li className='post-info--hidden'>
<PostInfo
post={post}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 824e7ef39..dba75ac5f 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -150,7 +150,7 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
- let tooltip = <Tooltip>{utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}</Tooltip>;
+ let tooltip = <Tooltip id={post.id + 'tooltip'}>{`${utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}`}</Tooltip>;
return (
<ul className='post-header post-info'>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 3e1e075bb..b90197ac4 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -128,6 +128,15 @@ export default class PostList extends React.Component {
this.userHasSeenNew = true;
}
this.isUserScroll = true;
+
+ $('.top-visible-post').removeClass('top-visible-post');
+
+ $(React.findDOMNode(this.refs.postlistcontent)).children().each(function select() {
+ if ($(this).position().top + $(this).height() / 2 > 0) {
+ $(this).addClass('top-visible-post');
+ return false;
+ }
+ });
});
$('.post-list__content div .post').removeClass('post--last');
@@ -507,8 +516,19 @@ export default class PostList extends React.Component {
sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
- // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
- hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post);
+ // hide the profile pic if:
+ // the previous post was made by the same user as the current post,
+ // the previous post is not a comment,
+ // the current post is not a comment,
+ // the current post is not from a webhook
+ // and the previous post is not from a webhook
+ if ((prevPost.user_id === post.user_id) &&
+ !utils.isComment(prevPost) &&
+ !utils.isComment(post) &&
+ (!post.props || !post.props.from_webhook) &&
+ (!prevPost.props || !prevPost.props.from_webhook)) {
+ hideProfilePic = true;
+ }
}
// check if it's the last comment in a consecutive string of comments on the same post
@@ -665,7 +685,10 @@ export default class PostList extends React.Component {
className={'post-list-holder-by-time ' + activeClass}
>
<div className='post-list__table'>
- <div className='post-list__content'>
+ <div
+ ref='postlistcontent'
+ className='post-list__content'
+ >
{moreMessages}
{postCtls}
</div>
diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx
index 37958b649..9d514c741 100644
--- a/web/react/components/rename_channel_modal.jsx
+++ b/web/react/components/rename_channel_modal.jsx
@@ -78,7 +78,6 @@ export default class RenameChannelModal extends React.Component {
$(React.findDOMNode(this.refs.modal)).modal('hide');
AsyncClient.getChannel(channel.id);
- Utils.updateTabTitle(channel.display_name);
Utils.updateAddressBar(channel.name);
React.findDOMNode(this.refs.displayName).value = '';
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index 32b521560..bdefdbee8 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -47,13 +47,8 @@ export default class SearchResultsItem extends React.Component {
);
var postChannel = ChannelStore.get(this.props.post.channel_id);
- var teammate = '';
- if (postChannel.type === 'D') {
- teammate = utils.getDirectTeammate(this.props.post.channel_id).username;
- }
-
- utils.switchChannel(postChannel, teammate);
+ utils.switchChannel(postChannel);
}
render() {
diff --git a/web/react/components/setting_upload.jsx b/web/react/components/setting_upload.jsx
index fad27b355..ccb26cc58 100644
--- a/web/react/components/setting_upload.jsx
+++ b/web/react/components/setting_upload.jsx
@@ -7,11 +7,11 @@ export default class SettingsUpload extends React.Component {
this.doFileSelect = this.doFileSelect.bind(this);
this.doSubmit = this.doSubmit.bind(this);
- this.onFileSelect = this.onFileSelect.bind(this);
this.state = {
clientError: this.props.clientError,
- serverError: this.props.serverError
+ serverError: this.props.serverError,
+ filename: ''
};
}
@@ -24,9 +24,14 @@ export default class SettingsUpload extends React.Component {
doFileSelect(e) {
e.preventDefault();
+ var filename = $(e.target).val();
+ if (filename.substring(3, 11) === 'fakepath') {
+ filename = filename.substring(12);
+ }
this.setState({
clientError: '',
- serverError: ''
+ serverError: '',
+ filename
});
}
@@ -40,28 +45,28 @@ export default class SettingsUpload extends React.Component {
}
}
- onFileSelect(e) {
- var filename = $(e.target).val();
- if (filename.substring(3, 11) === 'fakepath') {
- filename = filename.substring(12);
- }
- $(e.target).closest('li').find('.file-status').addClass('hide');
- $(e.target).closest('li').find('.file-name').removeClass('hide').html(filename);
- }
-
render() {
- var clientError = null;
+ let clientError = null;
if (this.state.clientError) {
clientError = (
<div className='file-status'>{this.state.clientError}</div>
);
}
- var serverError = null;
+ let serverError = null;
if (this.state.serverError) {
serverError = (
<div className='file-status'>{this.state.serverError}</div>
);
}
+ let fileNameText = null;
+ let submitButtonClass = 'btn btn-sm btn-primary disabled';
+ if (this.state.filename) {
+ fileNameText = (
+ <div className='file-status file-name'>{this.state.filename}</div>
+ );
+ submitButtonClass = 'btn btn-sm btn-primary';
+ }
+
return (
<ul className='section-max'>
<li className='col-sm-12 section-title'>{this.props.title}</li>
@@ -70,21 +75,21 @@ export default class SettingsUpload extends React.Component {
<ul className='setting-list'>
<li className='setting-list-item'>
<span className='btn btn-sm btn-primary btn-file sel-btn'>
- Select file
+ {'Select file'}
<input
ref='uploadinput'
accept={this.props.fileTypesAccepted}
type='file'
- onChange={this.onFileSelect}
+ onChange={this.doFileSelect}
/>
</span>
<a
- className={'btn btn-sm btn-primary'}
+ className={submitButtonClass}
onClick={this.doSubmit}
>
- Import
+ {'Import'}
</a>
- <div className='file-status file-name hide'></div>
+ {fileNameText}
{serverError}
{clientError}
</li>
@@ -102,4 +107,4 @@ SettingsUpload.propTypes = {
clientError: React.PropTypes.string,
serverError: React.PropTypes.string,
helpText: React.PropTypes.object
-};
+}; \ No newline at end of file
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 14664ed4d..c0841a508 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -200,13 +200,17 @@ export default class Sidebar extends React.Component {
}
var channel = ChannelStore.get(msg.channel_id);
- var user = UserStore.getCurrentUser();
- if (user.notify_props && ((user.notify_props.desktop === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') || user.notify_props.desktop === 'none')) {
- return;
+ const user = UserStore.getCurrentUser();
+ const member = ChannelStore.getMember(msg.channel_id);
+
+ var notifyLevel = member.notify_props.desktop;
+ if (notifyLevel === 'default') {
+ notifyLevel = user.notify_props.desktop;
}
- var member = ChannelStore.getMember(msg.channel_id);
- if ((member.notify_level === 'mention' && mentions.indexOf(user.id) === -1) || member.notify_level === 'none' || member.notify_level === 'quiet') {
+ if (notifyLevel === 'none') {
+ return;
+ } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') {
return;
}
@@ -268,14 +272,19 @@ export default class Sidebar extends React.Component {
}
}
updateTitle() {
- var channel = ChannelStore.getCurrent();
+ const channel = ChannelStore.getCurrent();
if (channel) {
+ let currentSiteName = '';
+ if (global.window.config.SiteName != null) {
+ currentSiteName = global.window.config.SiteName;
+ }
+
+ let currentChannelName = channel.display_name;
if (channel.type === 'D') {
- var teammateUsername = Utils.getDirectTeammate(channel.id).username;
- document.title = teammateUsername + ' ' + document.title.substring(document.title.lastIndexOf('-'));
- } else {
- document.title = channel.display_name + ' ' + document.title.substring(document.title.lastIndexOf('-'));
+ currentChannelName = Utils.getDirectTeammate(channel.id).username;
}
+
+ document.title = currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName;
}
}
onScroll() {
@@ -325,7 +334,7 @@ export default class Sidebar extends React.Component {
var unread = false;
if (channelMember) {
msgCount = channel.total_msg_count - channelMember.msg_count;
- unread = (msgCount > 0 && channelMember.notify_level !== 'quiet') || channelMember.mention_count > 0;
+ unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0;
}
var titleClass = '';
@@ -503,6 +512,7 @@ export default class Sidebar extends React.Component {
/>
<SidebarHeader
teamDisplayName={this.props.teamDisplayName}
+ teamName={this.props.teamName}
teamType={this.props.teamType}
/>
<SearchBox />
@@ -582,5 +592,6 @@ Sidebar.defaultProps = {
};
Sidebar.propTypes = {
teamType: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string
+ teamDisplayName: React.PropTypes.string,
+ teamName: React.PropTypes.string
};
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 072c14e0a..33de35064 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -52,6 +52,8 @@ export default class SidebarHeader extends React.Component {
<NavbarDropdown
ref='dropdown'
teamType={this.props.teamType}
+ teamDisplayName={this.props.teamDisplayName}
+ teamName={this.props.teamName}
/>
</div>
);
@@ -64,5 +66,6 @@ SidebarHeader.defaultProps = {
};
SidebarHeader.propTypes = {
teamDisplayName: React.PropTypes.string,
+ teamName: React.PropTypes.string,
teamType: React.PropTypes.string
};
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index 913715154..708cd04cb 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -14,9 +14,10 @@ export default class SidebarRight extends React.Component {
constructor(props) {
super(props);
+ this.plScrolledToBottom = true;
+
this.onSelectedChange = this.onSelectedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
- this.resize = this.resize.bind(this);
this.state = getStateFromStores();
}
@@ -28,6 +29,14 @@ export default class SidebarRight extends React.Component {
PostStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
}
+ componentDidUpdate() {
+ if (!this.plScrolledToBottom) {
+ $('.top-visible-post')[0].scrollIntoView();
+ } else {
+ var postHolder = $('.post-list-holder-by-time').not('.inactive');
+ postHolder.scrollTop(postHolder[0].scrollHeight);
+ }
+ }
onSelectedChange(fromSearch) {
var newState = getStateFromStores(fromSearch);
newState.from_search = fromSearch;
@@ -41,15 +50,15 @@ export default class SidebarRight extends React.Component {
this.setState(newState);
}
}
- resize() {
- var postHolder = $('.post-list-holder-by-time');
- postHolder[0].scrollTop = postHolder[0].scrollHeight - 224;
- }
render() {
+ var postHolder = $('.post-list-holder-by-time').not('.inactive');
+ const position = postHolder.scrollTop() + postHolder.height() + 14;
+ const bottom = postHolder[0].scrollHeight;
+ this.plScrolledToBottom = position >= bottom;
+
if (!(this.state.search_visible || this.state.post_right_visible)) {
$('.inner__wrap').removeClass('move--left').removeClass('move--right');
$('.sidebar--right').removeClass('move--left');
- this.resize();
return (
<div></div>
);
@@ -59,8 +68,8 @@ export default class SidebarRight extends React.Component {
$('.sidebar--left').removeClass('move--right');
$('.sidebar--right').addClass('move--left');
$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
- this.resize();
- setTimeout(function overlayTimer() {
+
+ setTimeout(() => {
$('.sidebar__overlay').fadeOut('200', function fadeOverlay() {
$(this).remove();
});
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index f1341d9d7..2df2c8ffd 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -26,11 +26,14 @@ export default class SidebarRightMenu extends React.Component {
var inviteLink = '';
var teamSettingsLink = '';
var manageLink = '';
+ var consoleLink = '';
var currentUser = UserStore.getCurrentUser();
var isAdmin = false;
+ var isSystemAdmin = false;
if (currentUser != null) {
isAdmin = utils.isAdmin(currentUser.roles);
+ isSystemAdmin = utils.isSystemAdmin(currentUser.roles);
inviteLink = (
<li>
@@ -77,6 +80,17 @@ export default class SidebarRightMenu extends React.Component {
);
}
+ if (isSystemAdmin) {
+ consoleLink = (
+ <li>
+ <a
+ href='/admin_console'
+ >
+ <i className='glyphicon glyphicon-wrench'></i>System Console</a>
+ </li>
+ );
+ }
+
var siteName = '';
if (global.window.config.SiteName != null) {
siteName = global.window.config.SiteName;
@@ -107,6 +121,7 @@ export default class SidebarRightMenu extends React.Component {
{inviteLink}
{teamLink}
{manageLink}
+ {consoleLink}
<li>
<a
href='#'
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 1b722d611..a3f89a217 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -35,8 +35,8 @@ export default class TeamSignupUrlPage extends React.Component {
if (cleanedName !== name || !urlRegex.test(name)) {
this.setState({nameError: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."});
return;
- } else if (cleanedName.length <= 3 || cleanedName.length > 15) {
- this.setState({nameError: 'Name must be 4 or more characters up to a maximum of 15'});
+ } else if (cleanedName.length <= 2 || cleanedName.length > 15) {
+ this.setState({nameError: 'Name must be 3 or more characters up to a maximum of 15'});
return;
}
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index a4972dd8d..14f281f7a 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -23,12 +23,14 @@ export default class SSOSignUpPage extends React.Component {
team.display_name = this.state.name;
- if (team.display_name.length <= 3) {
+ if (!team.display_name) {
+ state.nameError = 'Please enter a team name';
+ this.setState(state);
return;
}
- if (!team.display_name) {
- state.nameError = 'Please enter a team name';
+ if (team.display_name.length <= 2) {
+ state.nameError = 'Name must be 3 or more characters up to a maximum of 15';
this.setState(state);
return;
}
@@ -68,7 +70,7 @@ export default class SSOSignUpPage extends React.Component {
}
var disabled = false;
- if (this.state.name.length <= 3) {
+ if (this.state.name.length <= 2) {
disabled = true;
}
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index c5d028d31..ceb8f52a7 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -31,8 +31,10 @@ export default class UserProfile extends React.Component {
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
- $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}});
- $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
+ if (!this.props.disablePopover) {
+ $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}});
+ $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
+ }
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
@@ -56,6 +58,10 @@ export default class UserProfile extends React.Component {
name = this.props.overwriteName;
}
+ if (this.props.disablePopover) {
+ return <div>{name}</div>;
+ }
+
var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />';
if (!global.window.config.ShowEmailAddress === 'true') {
dataContent += '<div class="text-nowrap">Email not shared</div>';
@@ -79,9 +85,11 @@ export default class UserProfile extends React.Component {
UserProfile.defaultProps = {
userId: '',
- overwriteName: ''
+ overwriteName: '',
+ disablePopover: false
};
UserProfile.propTypes = {
userId: React.PropTypes.string,
- overwriteName: React.PropTypes.string
+ overwriteName: React.PropTypes.string,
+ disablePopover: React.PropTypes.bool
};
diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx
index 48be83afe..0f5cb59f5 100644
--- a/web/react/components/user_settings/import_theme_modal.jsx
+++ b/web/react/components/user_settings/import_theme_modal.jsx
@@ -140,7 +140,7 @@ export default class ImportThemeModal extends React.Component {
>
<Modal.Body>
<p>
- {'To import a theme, go to a Slack team and look for “”Preferences” -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:'}
+ {'To import a theme, go to a Slack team and look for “Preferences -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:'}
</p>
<div className='form-group less'>
<div className='col-sm-9'>
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
index 1bbfbd162..899dbcd05 100644
--- a/web/react/components/user_settings/manage_incoming_hooks.jsx
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -21,7 +21,7 @@ export default class ManageIncomingHooks extends React.Component {
this.getHooks();
}
addNewHook() {
- let hook = {}; //eslint-disable-line prefer-const
+ const hook = {};
hook.channel_id = this.state.channelId;
Client.addIncomingHook(
@@ -40,13 +40,13 @@ export default class ManageIncomingHooks extends React.Component {
);
}
removeHook(id) {
- let data = {}; //eslint-disable-line prefer-const
+ const data = {};
data.id = id;
Client.deleteIncomingHook(
data,
() => {
- let hooks = this.state.hooks; //eslint-disable-line prefer-const
+ const hooks = this.state.hooks;
let index = -1;
for (let i = 0; i < hooks.length; i++) {
if (hooks[i].id === id) {
@@ -69,7 +69,7 @@ export default class ManageIncomingHooks extends React.Component {
getHooks() {
Client.listIncomingHooks(
(data) => {
- let state = this.state; //eslint-disable-line prefer-const
+ const state = this.state;
if (data) {
state.hooks = data;
@@ -93,9 +93,11 @@ export default class ManageIncomingHooks extends React.Component {
}
const channels = ChannelStore.getAll();
- let options = []; //eslint-disable-line prefer-const
+ const options = [];
channels.forEach((channel) => {
- options.push(<option value={channel.id}>{channel.name}</option>);
+ if (channel.type !== Constants.DM_CHANNEL) {
+ options.push(<option value={channel.id}>{channel.name}</option>);
+ }
});
let disableButton = '';
@@ -103,7 +105,7 @@ export default class ManageIncomingHooks extends React.Component {
disableButton = ' disable';
}
- let hooks = []; //eslint-disable-line prefer-const
+ const hooks = [];
this.state.hooks.forEach((hook) => {
const c = ChannelStore.get(hook.channel_id);
hooks.push(
@@ -146,6 +148,8 @@ export default class ManageIncomingHooks extends React.Component {
return (
<div key='addIncomingHook'>
+ {'Create webhook URLs for channels and private groups. These URLs can be used by outside applications to create posts in any channels or private groups you have access to. The specified channel will be used as the default.'}
+ <br/>
<label className='control-label'>{'Add a new incoming webhook'}</label>
<div className='padding-top'>
<select
diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx
index f8f916bd0..8116bffcc 100644
--- a/web/react/components/user_settings/premade_theme_chooser.jsx
+++ b/web/react/components/user_settings/premade_theme_chooser.jsx
@@ -24,7 +24,7 @@ export default class PremadeThemeChooser extends React.Component {
premadeThemes.push(
<div
- className='col-sm-3 premade-themes'
+ className='col-xs-6 col-sm-3 premade-themes'
key={'premade-theme-key' + k}
>
<div
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index 4372069e7..c4a137ed8 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -220,7 +220,7 @@ export default class UserSettingsAppearance extends React.Component {
className='theme'
onClick={this.handleImportModal}
>
- {'Import from Slack'}
+ {'Import theme colors from Slack'}
</a>
</div>
);
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index ba14f019f..e83f18aab 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -17,7 +17,7 @@ function getNotificationsStateFromStores() {
if (user.notify_props && user.notify_props.desktop_sound) {
sound = user.notify_props.desktop_sound;
}
- var desktop = 'all';
+ var desktop = 'default';
if (user.notify_props && user.notify_props.desktop) {
desktop = user.notify_props.desktop;
}
@@ -420,7 +420,7 @@ export default class NotificationsTab extends React.Component {
</label>
<br/>
</div>
- <div><br/>{'Email notifications are sent for mentions and direct messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div>
+ <div><br/>{'Email notifications are sent for mentions and direct messages after you’ve been offline for more than 60 seconds or away from ' + global.window.config.SiteName + ' for more than 5 minutes.'}</div>
</div>
);
diff --git a/web/react/pages/authorize.jsx b/web/react/pages/authorize.jsx
index db42c8266..8ea8b13eb 100644
--- a/web/react/pages/authorize.jsx
+++ b/web/react/pages/authorize.jsx
@@ -3,16 +3,16 @@
var Authorize = require('../components/authorize.jsx');
-function setupAuthorizePage(teamName, appName, responseType, clientId, redirectUri, scope, state) {
+function setupAuthorizePage(props) {
React.render(
<Authorize
- teamName={teamName}
- appName={appName}
- responseType={responseType}
- clientId={clientId}
- redirectUri={redirectUri}
- scope={scope}
- state={state}
+ teamName={props.TeamName}
+ appName={props.AppName}
+ responseType={props.ResponseType}
+ clientId={props.ClientId}
+ redirectUri={props.RedirectUri}
+ scope={props.Scope}
+ state={props.State}
/>,
document.getElementById('authorize')
);
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 74259194a..c333fd57d 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -36,11 +36,14 @@ var RemovedFromChannelModal = require('../components/removed_from_channel_modal.
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
function setupChannelPage(props) {
+ TeamStore.setCurrentId(props.TeamId);
+
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
name: props.ChannelName,
@@ -71,6 +74,7 @@ function setupChannelPage(props) {
React.render(
<Sidebar
teamDisplayName={props.TeamDisplayName}
+ teamName={props.TeamName}
teamType={props.TeamType}
/>,
document.getElementById('sidebar-left')
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index ab2965000..7db3ef30d 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -152,21 +152,23 @@ export function getChannel(id) {
}
export function updateLastViewedAt() {
- if (isCallInProgress('updateLastViewed')) {
+ const channelId = ChannelStore.getCurrentId();
+
+ if (channelId === null) {
return;
}
- if (ChannelStore.getCurrentId() == null) {
+ if (isCallInProgress(`updateLastViewed${channelId}`)) {
return;
}
- callTracker.updateLastViewed = utils.getTimestamp();
+ callTracker[`updateLastViewed${channelId}`] = utils.getTimestamp();
client.updateLastViewedAt(
- ChannelStore.getCurrentId(),
- function updateLastViewedAtSuccess() {
+ channelId,
+ () => {
callTracker.updateLastViewed = 0;
},
- function updateLastViewdAtFailure(err) {
+ (err) => {
callTracker.updateLastViewed = 0;
dispatchError(err, 'updateLastViewedAt');
}
@@ -634,4 +636,4 @@ export function getMyTeam() {
dispatchError(err, 'getMyTeam');
}
);
-} \ No newline at end of file
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 715e26197..5cb165b4c 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -332,6 +332,20 @@ export function saveConfig(config, success, error) {
});
}
+export function logClientError(msg) {
+ var l = {};
+ l.level = 'ERROR';
+ l.message = msg;
+
+ $.ajax({
+ url: '/api/v1/admin/log_client',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(l)
+ });
+}
+
export function testEmail(config, success, error) {
$.ajax({
url: '/api/v1/admin/test_email',
@@ -568,16 +582,16 @@ export function updateChannelDesc(data, success, error) {
track('api', 'api_channels_desc');
}
-export function updateNotifyLevel(data, success, error) {
+export function updateNotifyProps(data, success, error) {
$.ajax({
- url: '/api/v1/channels/update_notify_level',
+ url: '/api/v1/channels/update_notify_props',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: function onError(xhr, status, err) {
- var e = handleError('updateNotifyLevel', xhr, status, err);
+ var e = handleError('updateNotifyProps', xhr, status, err);
error(e);
}
});
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 75e80bc7e..67414dc3b 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -111,6 +111,7 @@ module.exports = {
],
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
MAX_DMS: 20,
+ DM_CHANNEL: 'D',
MAX_POST_LEN: 4000,
EMOJI_SIZE: 16,
ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
@@ -178,7 +179,7 @@ module.exports = {
centerChannelColor: '#DDDDDD',
newMessageSeparator: '#5de5da',
linkColor: '#A4FFEB',
- buttonBg: '#1dacfc',
+ buttonBg: '#4CBBA4',
buttonColor: '#FFFFFF'
},
windows10: {
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 56bf49c3f..34e42cbae 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -87,8 +87,10 @@ function autolinkUrls(text, tokens) {
const linkText = match.getMatchedText();
let url = linkText;
- if (url.lastIndexOf('http', 0) !== 0) {
- url = `http://${linkText}`;
+ if (match.getType() === 'email') {
+ url = `mailto:${url}`;
+ } else if (url.lastIndexOf('http', 0) !== 0) {
+ url = `http://${url}`;
}
const index = tokens.size;
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 556f3967c..1bc082175 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -78,6 +78,14 @@ export function isAdmin(roles) {
return false;
}
+export function isSystemAdmin(roles) {
+ if (isInRole(roles, 'system_admin')) {
+ return true;
+ }
+
+ return false;
+}
+
export function getDomainWithOutSub() {
var parts = window.location.host.split('.');
@@ -255,163 +263,6 @@ export function escapeRegExp(string) {
return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
}
-function handleYoutubeTime(link) {
- var timeRegex = /[\\?&]t=([0-9hms]+)/;
-
- var time = link.trim().match(timeRegex);
- if (!time || !time[1]) {
- return '';
- }
-
- var hours = time[1].match(/([0-9]+)h/);
- var minutes = time[1].match(/([0-9]+)m/);
- var seconds = time[1].match(/([0-9]+)s/);
-
- var ticks = 0;
-
- if (hours && hours[1]) {
- ticks += parseInt(hours[1], 10) * 3600;
- }
-
- if (minutes && minutes[1]) {
- ticks += parseInt(minutes[1], 10) * 60;
- }
-
- if (seconds && seconds[1]) {
- ticks += parseInt(seconds[1], 10);
- }
-
- return '&start=' + ticks.toString();
-}
-
-function getYoutubeEmbed(link) {
- var regex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
-
- var youtubeId = link.trim().match(regex)[1];
- var time = handleYoutubeTime(link);
-
- function onClick(e) {
- var div = $(e.target).closest('.video-thumbnail__container')[0];
- var iframe = document.createElement('iframe');
- iframe.setAttribute('src',
- 'https://www.youtube.com/embed/' +
- div.id +
- '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1' +
- time);
- iframe.setAttribute('width', '480px');
- iframe.setAttribute('height', '360px');
- iframe.setAttribute('type', 'text/html');
- iframe.setAttribute('frameborder', '0');
- iframe.setAttribute('allowfullscreen', 'allowfullscreen');
-
- div.parentNode.replaceChild(iframe, div);
- }
-
- function success(data) {
- if (!data.items.length || !data.items[0].snippet) {
- return;
- }
- var metadata = data.items[0].snippet;
- $('.video-type.' + youtubeId).html('Youtube - ');
- $('.video-uploader.' + youtubeId).html(metadata.channelTitle);
- $('.video-title.' + youtubeId).find('a').html(metadata.title);
- }
-
- if (global.window.config.GoogleDeveloperKey) {
- $.ajax({
- async: true,
- url: 'https://www.googleapis.com/youtube/v3/videos',
- type: 'GET',
- data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey},
- success: success
- });
- }
-
- return (
- <div className='post-comment'>
- <h4>
- <span className={'video-type ' + youtubeId}>YouTube</span>
- <span className={'video-title ' + youtubeId}><a href={link}></a></span>
- </h4>
- <h4 className={'video-uploader ' + youtubeId}></h4>
- <div
- className='video-div embed-responsive-item'
- id={youtubeId}
- onClick={onClick}
- >
- <div className='embed-responsive embed-responsive-4by3 video-div__placeholder'>
- <div
- id={youtubeId}
- className='video-thumbnail__container'
- >
- <img
- className='video-thumbnail'
- src={'https://i.ytimg.com/vi/' + youtubeId + '/hqdefault.jpg'}
- />
- <div className='block'>
- <span className='play-button'><span/></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
-}
-
-export function getEmbed(link) {
- var ytRegex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
-
- var match = link.trim().match(ytRegex);
- if (match && match[1].length === 11) {
- return getYoutubeEmbed(link);
- }
-
- // Generl embed feature turned off for now
- return '';
-
- // NEEDS REFACTORING WHEN TURNED BACK ON
- /*
- var id = parseInt((Math.random() * 1000000) + 1);
-
- $.ajax({
- type: 'GET',
- url: 'https://query.yahooapis.com/v1/public/yql',
- data: {
- q: 'select * from html where url="' + link + "\" and xpath='html/head'",
- format: 'json'
- },
- async: true
- }).done(function(data) {
- if(!data.query.results) {
- return;
- }
-
- var headerData = data.query.results.head;
-
- var description = ''
- for(var i = 0; i < headerData.meta.length; i++) {
- if(headerData.meta[i].name && (headerData.meta[i].name === 'description' || headerData.meta[i].name === 'Description')){
- description = headerData.meta[i].content;
- break;
- }
- }
-
- $('.embed-title.'+id).html(headerData.title);
- $('.embed-description.'+id).html(description);
- })
-
- return (
- <div className='post-comment'>
- <div className={'web-embed-data'}>
- <p className={'embed-title ' + id} />
- <p className={'embed-description ' + id} />
- <p className={'embed-link ' + id}>{link}</p>
- </div>
- </div>
- );
- */
-}
-
export function areStatesEqual(state1, state2) {
return JSON.stringify(state1) === JSON.stringify(state2);
}
@@ -544,35 +395,39 @@ export function toTitleCase(str) {
export function applyTheme(theme) {
if (theme.sidebarBg) {
- changeCss('.sidebar--left', 'background:' + theme.sidebarBg, 1);
+ changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1);
}
if (theme.sidebarText) {
- changeCss('.sidebar--left .nav li>a, .sidebar--right', 'color:' + theme.sidebarText, 1);
- changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
+ changeCss('.sidebar--left .nav-pills__container li>a, .sidebar--right, .settings-modal .nav-pills>li a, .sidebar--menu', 'color:' + theme.sidebarText, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1);
changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
}
if (theme.sidebarUnreadText) {
- changeCss('.sidebar--left .nav li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 1);
+ changeCss('.sidebar--left .nav-pills__container li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 2);
}
if (theme.sidebarTextHoverBg) {
- changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
}
if (theme.sidebarTextHoverColor) {
- changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'color:' + theme.sidebarTextHoverColor, 2);
+ changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'color:' + theme.sidebarTextHoverColor, 2);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'color:' + theme.sidebarTextHoverColor, 2);
}
if (theme.sidebarTextActiveBg) {
- changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + theme.sidebarTextActiveBg, 1);
+ changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'background:' + theme.sidebarTextActiveBg, 1);
}
if (theme.sidebarTextActiveColor) {
- changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'color:' + theme.sidebarTextActiveColor, 2);
+ changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2);
}
if (theme.sidebarHeaderBg) {
@@ -607,41 +462,51 @@ export function applyTheme(theme) {
}
if (theme.centerChannelBg) {
- changeCss('.app__content, .markdown__table, .markdown__table tbody tr', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box, .modal .modal-content, .mentions-name, .mentions--top .mentions-box', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
changeCss('.post-image__column .post-image__details', 'background:' + theme.centerChannelBg, 1);
- changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.sidebar--right, .dropdown-menu, .popover', 'background:' + theme.centerChannelBg, 1);
}
if (theme.centerChannelColor) {
- changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name', 'color:' + theme.centerChannelColor, 1);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
+ changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
+ changeCss('.mentions--top, .command-box', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1);
+ changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3);
+ changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2);
+ changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1);
changeCss('.post-body hr', 'background:' + theme.centerChannelColor, 1);
changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1);
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
- changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.command-name, .popover .popover-title', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.dropdown-menu .divider', 'background:' + theme.centerChannelColor, 1);
changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1);
changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
- changeCss('.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.search-bar__container .search__form .search-bar', 'background: transparent; color:' + theme.centerChannelColor, 1);
+ changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
- changeCss('.date-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
- changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
- changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.date-separator.hovered--before:after, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.date-separator.hovered--after:before, .new-separator.hovered--after:before', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post:hover, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
}
@@ -652,7 +517,7 @@ export function applyTheme(theme) {
}
if (theme.linkColor) {
- changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1);
+ changeCss('a, a:focus, a:hover, .btn, .btn:focus, .btn:hover', 'color:' + theme.linkColor, 1);
changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1);
}
@@ -789,16 +654,12 @@ export function isValidUsername(name) {
return error;
}
-export function updateTabTitle(name) {
- document.title = name + ' ' + document.title.substring(document.title.lastIndexOf('-'));
-}
-
export function updateAddressBar(channelName) {
var teamURL = window.location.href.split('/channels')[0];
history.replaceState('data', '', teamURL + '/channels/' + channelName);
}
-export function switchChannel(channel, teammateName) {
+export function switchChannel(channel) {
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
name: channel.name,
@@ -807,12 +668,6 @@ export function switchChannel(channel, teammateName) {
updateAddressBar(channel.name);
- if (channel.type === 'D' && teammateName) {
- updateTabTitle(teammateName);
- } else {
- updateTabTitle(channel.display_name);
- }
-
AsyncClient.getChannels(true, true, true);
AsyncClient.getChannelExtraInfo(true);
AsyncClient.getPosts(channel.id);
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 11794a269..5037da415 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -144,7 +144,6 @@
}
.popover {
border-radius: 3px;
- border: 1px solid #ccc;
width: 100%;
font-size: 0.95em;
}
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 87d9b8200..fa465ff91 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -42,6 +42,21 @@ body {
color: $primary-color;
}
}
+ .popover-title {
+ background: rgba(black, 0.1);
+ }
+}
+
+.dropdown-menu {
+ .divider {
+ @include opacity(0.15);
+ }
+ > li > a {
+ color: inherit;
+ &:focus, &:hover {
+ color: inherit;
+ }
+ }
}
.word-break--all {
@@ -68,6 +83,19 @@ a:focus, a:hover {
margin: 0;
}
+.text-danger {
+ color: #E05F5D;
+}
+
+.btn {
+ &.btn-danger {
+ color: #fff;
+ &:hover, &:active {
+ color: #fff;
+ }
+ }
+}
+
.form-control {
@include border-radius(2px);
&.no-resize {
diff --git a/web/sass-files/sass/partials/_command-box.scss b/web/sass-files/sass/partials/_command-box.scss
index 44eb9b8df..f1aa4dca2 100644
--- a/web/sass-files/sass/partials/_command-box.scss
+++ b/web/sass-files/sass/partials/_command-box.scss
@@ -4,20 +4,29 @@
width: 100%;
border: $border-gray;
bottom: 38px;
+ overflow: auto;
@extend %popover-box-shadow;
+ .sidebar--right & {
+ bottom: 100px;
+ }
}
.command-name {
position: relative;
width: 100%;
- background-color: #fff;
- height: 37px;
- line-height: 37px;
- padding: 2px 10px 2px 5px;
+ line-height: 24px;
+ padding: 5px 10px 8px;
z-index: 101;
+ font-size: 0.95em;
+ border-bottom: 1px solid #ddd;
&:hover {
background-color: #e8eaed;
}
+ .command__desc {
+ margin-left: 5px;
+ @include opacity(0.5);
+ line-height: normal;
+ }
}
.command-desc {
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index b5fcb6145..9b9e5f573 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -114,23 +114,6 @@
}
}
}
- &.theme--black {
- &:hover {
- &:before {
- background: rgba(white, 0.2);
- }
- }
- }
- &.theme--gray {
- &:hover {
- &:before {
- background: rgba(white, 0.1);
- }
- }
- }
- a {
- color: #fff;
- }
.navbar-right {
font-size: 0.85em;
position: absolute;
@@ -145,7 +128,6 @@
.dropdown-menu {
li a {
padding: 3px 20px;
- color: #555;
text-overflow: ellipsis;
overflow: hidden;
}
diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss
index 48746ba01..aff31e418 100644
--- a/web/sass-files/sass/partials/_mentions.scss
+++ b/web/sass-files/sass/partials/_mentions.scss
@@ -33,13 +33,6 @@
line-height: 36px;
font-size: 13px;
cursor: pointer;
- &:hover {
- background-color: #E6F2FA;
- }
-}
-
-.mentions-focus {
- background-color: #E6F2FA;
}
.mentions-text {
@@ -54,7 +47,6 @@
display: block;
font-size: 20px;
text-align: center;
- color: #555;
@include border-radius(32px);
}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index e4e8b20b6..96b26f251 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -9,12 +9,6 @@
@include opacity(0.7);
}
}
- a, a:focus, a:hover {
- color: #2389D7;
- &.text-danger {
- color: #a94442;
- }
- }
.custom-textarea {
color: inherit;
border-color: #ccc;
@@ -25,12 +19,9 @@
}
.btn {
font-size: 13px;
- &.btn-primary {
- background: #4285f4;
- &:hover, &:focus, &:active {
- background: $primary-color--hover;
- color: #fff;
- }
+ &.btn-default {
+ border: none;
+ background: transparent;
}
}
.info__label {
@@ -70,7 +61,7 @@
background: $primary-color;
color: #FFF;
padding: 15px 15px 11px;
- border: none;
+ border: 1px solid #ddd;
min-height: 56px;
@include clearfix;
.modal-title {
diff --git a/web/sass-files/sass/partials/_oauth.scss b/web/sass-files/sass/partials/_oauth.scss
new file mode 100644
index 000000000..35d3a95e3
--- /dev/null
+++ b/web/sass-files/sass/partials/_oauth.scss
@@ -0,0 +1,31 @@
+.oauth-prompt {
+ background: #fff;
+ border: 1px solid #ddd;
+ padding: 1em 2em 0;
+ margin: 50px auto;
+ max-width: 90%;
+ width: 600px;
+ .prompt__heading {
+ font-size: em(20px);
+ line-height: normal;
+ margin: 1em 0;
+ display: table;
+ width: 100%;
+ > div {
+ display: table-cell;
+ vertical-align: top;
+ }
+ img {
+ margin-right: 15px;
+ }
+ }
+ .prompt__allow {
+ margin: 1em 0;
+ font-size: em(24px);
+ }
+ .prompt__buttons {
+ text-align: right;
+ border-top: 1px solid #ddd;
+ padding: 1.5em 0;
+ }
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 7532875d6..8bf4b0534 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -509,3 +509,11 @@ body.ios {
}
}
}
+
+.bot-indicator {
+ background-color: lightgrey;
+ border-radius:2px;
+ padding-left:2px;
+ padding-right:2px;
+ font-family:"Courier New"
+}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 9e8d0dc7d..82ec1811a 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -402,12 +402,6 @@
margin-left: 7px;
}
}
- &.active, &:hover {
- a {
- color: #555;
- background: #fff;
- }
- }
}
}
}
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 3aab05d70..1f785f63c 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -2,7 +2,6 @@
@import "activity-log";
.user-settings {
- background: #fff;
min-height:300px;
.table-responsive {
max-width: 560px;
@@ -71,7 +70,7 @@
}
.section-max {
- background: #f2f2f2;
+ background: rgba(black, 0.05);
padding: 1em 0 1.3em;
margin-bottom: 0;
@include clearfix;
@@ -121,7 +120,7 @@
.fa {
margin-right: 7px;
font-size: 12px;
- color: #aaa;
+ @include opacity(0.5);
display: none;
}
}
@@ -131,7 +130,7 @@
}
.section-describe {
- color:grey;
+ @include opacity(0.7);
}
.divider-dark {
@@ -167,15 +166,11 @@
}
}
.control-label {
- color: #555;
font-weight: 600;
&.text-left {
text-align: left;
}
}
- hr {
- border-color: #ccc;
- }
}
.file-status {
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index 6a418e270..73d702fef 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -16,12 +16,6 @@
overflow-y: auto;
max-width: 200px;
width: 200px;
- a {
- color: #262626 !important;
- &:hover, &:focus {
- background: #f5f5f5 !important;
- }
- }
}
.search__form {
margin: 0;
@@ -75,7 +69,7 @@
top: 66px;
}
.nav-pills__unread-indicator-bottom {
- bottom: 0px;
+ bottom: 10px;
}
.nav {
@@ -98,7 +92,6 @@
padding: 3px 10px 3px 25px;
line-height: 1.5;
border-radius: 0;
- color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -109,12 +102,8 @@
text-decoration: underline;
}
&.unread-title {
- color: #333;
font-weight: 600;
}
- &:hover, &:focus {
- background: #e6f2fa;
- }
}
&.active {
a, a:hover, a:focus {
diff --git a/web/sass-files/sass/partials/_sidebar--menu.scss b/web/sass-files/sass/partials/_sidebar--menu.scss
index 4366b1a6c..6f4a0cc38 100644
--- a/web/sass-files/sass/partials/_sidebar--menu.scss
+++ b/web/sass-files/sass/partials/_sidebar--menu.scss
@@ -54,7 +54,7 @@
> a {
font-size: 15px;
background: none !important;
- color: #444;
+ color: inherit;
line-height: 40px;
padding: 0 15px;
.glyphicon {
diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss
index e704536a8..c614052da 100644
--- a/web/sass-files/sass/styles.scss
+++ b/web/sass-files/sass/styles.scss
@@ -41,3 +41,6 @@
// Responsive Css
@import "partials/responsive";
+
+// Standalone Css
+@import "partials/oauth";
diff --git a/web/static/images/themes/mattermost dark.png b/web/static/images/themes/mattermost dark.png
index 832f64d2e..1ab44e104 100644
--- a/web/static/images/themes/mattermost dark.png
+++ b/web/static/images/themes/mattermost dark.png
Binary files differ
diff --git a/web/static/images/themes/mattermost.png b/web/static/images/themes/mattermost.png
index 4a321adcb..8dea37ff6 100644
--- a/web/static/images/themes/mattermost.png
+++ b/web/static/images/themes/mattermost.png
Binary files differ
diff --git a/web/static/images/themes/organization.png b/web/static/images/themes/organization.png
index 1a38bfb34..3066cca0b 100644
--- a/web/static/images/themes/organization.png
+++ b/web/static/images/themes/organization.png
Binary files differ
diff --git a/web/static/images/themes/slack.png b/web/static/images/themes/slack.png
deleted file mode 100644
index dc70c7dc2..000000000
--- a/web/static/images/themes/slack.png
+++ /dev/null
Binary files differ
diff --git a/web/static/images/themes/windows dark.png b/web/static/images/themes/windows dark.png
index d65304820..1f665d882 100644
--- a/web/static/images/themes/windows dark.png
+++ b/web/static/images/themes/windows dark.png
Binary files differ
diff --git a/web/templates/authorize.html b/web/templates/authorize.html
index 3392c1b1e..b0fa3e475 100644
--- a/web/templates/authorize.html
+++ b/web/templates/authorize.html
@@ -1,26 +1,26 @@
{{define "authorize"}}
<html>
{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- {{.Props.TeamName}}
- </div>
- <div class="col-sm-12">
- <div id="authorize"></div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_authorize_page('{{ .Props.TeamName }}', '{{ .Props.AppName }}', '{{ .Props.ResponseType }}', '{{ .Props.ClientId }}', '{{ .Props.RedirectUri }}', '{{ .Props.Scope }}', '{{ .Props.State }}' );
- </script>
+<body>
+ <div class="container-fluid">
+ <div class="oauth-prompt">
+ <div class="prompt__heading">
+ <div class="prompt__app-icon">
+ <img src="/static/images/icon50x50.gif" width="50" height="50" alt="">
+ </div>
+ <div class="text">An application would like to connect to your {{.Props.TeamName}} account.</div>
+ </div>
+ <p>The app <strong>{{.Props.AppName}}</strong> would like the ability to access Mattermost on your behalf.</p>
+ <h2 class="prompt__allow">Allow <strong>{{.Props.AppName}}</strong> access?</h2>
+ <div class="prompt__buttons">
+ <input type="button" class="btn btn-link" value="Deny">
+ <input type="button" class="btn btn-primary" value="Allow">
+ </div>
+ </div>
+ </div>
+ <script>
+ window.setup_authorize_page('{{.Props}}');
+ </script>
</body>
</html>
{{end}}
diff --git a/web/templates/head.html b/web/templates/head.html
index 2b83119d8..faac4975a 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -43,13 +43,32 @@
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
<style id="antiClickjack">body{display:none !important;}</style>
+
+ <script>
+ window.onerror = function(msg, url, line, column, stack) {
+ var l = {};
+ l.level = 'ERROR';
+ l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url;
+
+ $.ajax({
+ url: '/api/v1/admin/log_client',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(l)
+ });
+ }
+ </script>
+
<script src="/static/js/bundle.js"></script>
+
<script type="text/javascript">
if (self === top) {
var blocker = document.getElementById("antiClickjack");
blocker.parentNode.removeChild(blocker);
}
</script>
+
<script type="text/javascript">
if (window.config.SegmentDeveloperKey != null && window.config.SegmentDeveloperKey !== "") {
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";
diff --git a/web/web.go b/web/web.go
index da7eff13d..a1bbf5a81 100644
--- a/web/web.go
+++ b/web/web.go
@@ -189,9 +189,40 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
}
+ // We still might be able to switch to this team because we've logged in before
+ if multiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil {
+ multiToken := multiCookie.Value
+
+ if len(multiToken) > 0 {
+ tokens := strings.Split(multiToken, " ")
+
+ for _, token := range tokens {
+ if sr := <-api.Srv.Store.Session().Get(token); sr.Err == nil {
+ s := sr.Data.(*model.Session)
+
+ if !s.IsExpired() && s.TeamId == team.Id {
+ w.Header().Set(model.HEADER_TOKEN, s.Token)
+ sessionCookie := &http.Cookie{
+ Name: model.SESSION_TOKEN,
+ Value: s.Token,
+ Path: "/",
+ MaxAge: model.SESSION_TIME_WEB_IN_SECS,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, sessionCookie)
+
+ http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusTemporaryRedirect)
+ return
+ }
+ }
+ }
+ }
+ }
+
page := NewHtmlTemplatePage("login", "Login")
page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["TeamName"] = teamName
+ page.Props["TeamName"] = team.Name
page.Render(c, w)
}
@@ -288,6 +319,10 @@ func logout(c *api.Context, w http.ResponseWriter, r *http.Request) {
func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
name := params["channelname"]
+ teamName := params["team"]
+
+ var team *model.Team
+ teamChan := api.Srv.Store.Team().Get(c.Session.TeamId)
var channelId string
if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil {
@@ -297,6 +332,19 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
channelId = result.Data.(string)
}
+ if tResult := <-teamChan; tResult.Err != nil {
+ c.Err = tResult.Err
+ return
+ } else {
+ team = tResult.Data.(*model.Team)
+ }
+
+ if team.Name != teamName {
+ l4g.Error("It appears you are logged into " + team.Name + ", but are trying to access " + teamName)
+ http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusFound)
+ return
+ }
+
if len(channelId) == 0 {
if strings.Index(name, "__") > 0 {
// It's a direct message channel that doesn't exist yet so let's create it
@@ -319,7 +367,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
// lets make sure the user is valid
if result := <-api.Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = result.Err
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId)
return
}
@@ -332,18 +380,10 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
}
- var team *model.Team
-
- if tResult := <-api.Srv.Store.Team().Get(c.Session.TeamId); tResult.Err != nil {
- c.Err = tResult.Err
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
page := NewHtmlTemplatePage("channel", "")
page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientProps["SiteName"]
page.Props["TeamDisplayName"] = team.DisplayName
+ page.Props["TeamName"] = team.Name
page.Props["TeamType"] = team.Type
page.Props["TeamId"] = team.Id
page.Props["ChannelName"] = name
@@ -451,6 +491,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("password_reset", "")
page.Props["Title"] = "Reset Password " + page.ClientProps["SiteName"]
page.Props["TeamDisplayName"] = teamDisplayName
+ page.Props["TeamName"] = teamName
page.Props["Hash"] = hash
page.Props["Data"] = data
page.Props["TeamName"] = teamName
@@ -843,6 +884,12 @@ func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
+ c.Err = model.NewAppError("incomingWebhook", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
params := mux.Vars(r)
id := params["id"]
@@ -850,7 +897,12 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
- props := model.MapFromJson(strings.NewReader(r.FormValue("payload")))
+ var props map[string]string
+ if r.Header.Get("Content-Type") == "application/json" {
+ props = model.MapFromJson(r.Body)
+ } else {
+ props = model.MapFromJson(strings.NewReader(r.FormValue("payload")))
+ }
text := props["text"]
if len(text) == 0 {
@@ -860,6 +912,9 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
channelName := props["channel"]
+ overrideUsername := props["username"]
+ overrideIconUrl := props["icon_url"]
+
var hook *model.IncomingWebhook
if result := <-hchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Invalid webhook", "err="+result.Err.Message)
@@ -905,6 +960,15 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId)
post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text}
+ post.AddProp("from_webhook", "true")
+
+ if len(overrideUsername) != 0 && utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
+ post.AddProp("override_username", overrideUsername)
+ }
+
+ if len(overrideIconUrl) != 0 && utils.Cfg.ServiceSettings.EnablePostIconOverride {
+ post.AddProp("override_icon_url", overrideIconUrl)
+ }
if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "")