diff --git a/.idea/modules.xml b/.idea/modules.xml index d083fa11fd8a..95e982404989 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -810,6 +810,7 @@ + diff --git a/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy b/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy index a9acb7d39230..b5e08d05e9f6 100644 --- a/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy +++ b/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy @@ -68,6 +68,7 @@ abstract class BaseIdeaProperties extends JetBrainsProductProperties { "intellij.vcs.changeReminder", "intellij.filePrediction", "intellij.markdown", + "intellij.webp", "intellij.grazie" ] protected static final Map CE_CLASS_VERSIONS = [ diff --git a/intellij.idea.community.main.iml b/intellij.idea.community.main.iml index 3d191da8b5f2..a1a6c3769c05 100644 --- a/intellij.idea.community.main.iml +++ b/intellij.idea.community.main.iml @@ -139,5 +139,6 @@ + \ No newline at end of file diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/CommunityRepositoryModules.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/CommunityRepositoryModules.groovy index b154b4402386..a5c6fdd32396 100644 --- a/platform/build-scripts/groovy/org/jetbrains/intellij/build/CommunityRepositoryModules.groovy +++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/CommunityRepositoryModules.groovy @@ -89,6 +89,11 @@ class CommunityRepositoryModules { plugin("intellij.laf.macos") { bundlingRestrictions.supportedOs = [OsFamily.MACOS] }, + plugin("intellij.webp"){ + withResource("lib/libwebp/linux", "lib/libwebp/linux") + withResource("lib/libwebp/mac", "lib/libwebp/mac") + withResource("lib/libwebp/win", "lib/libwebp/win") + }, plugin("intellij.laf.win10") { bundlingRestrictions.supportedOs = [OsFamily.WINDOWS] }, @@ -360,10 +365,6 @@ class CommunityRepositoryModules { withResourceFromModule("intellij.android.core", "lib/sampleData", "lib/sampleData") withResourceArchive("../android/annotations", "lib/androidAnnotations.jar") - withResourceFromModule("intellij.android.adt.ui", "lib/libwebp/linux", "lib/libwebp/linux") - withResourceFromModule("intellij.android.adt.ui", "lib/libwebp/mac", "lib/libwebp/mac") - withResourceFromModule("intellij.android.adt.ui", "lib/libwebp/win", "lib/libwebp/win") - // here go some differences from original Android Studio layout def getSingleFile = { BuildContext context, String projectLibName -> List libFiles = context.project.libraryCollection diff --git a/plugins/webp/intellij.webp.iml b/plugins/webp/intellij.webp.iml new file mode 100644 index 000000000000..7a6585c31894 --- /dev/null +++ b/plugins/webp/intellij.webp.iml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/webp/lib/BUILD b/plugins/webp/lib/BUILD new file mode 100644 index 000000000000..620764c10d86 --- /dev/null +++ b/plugins/webp/lib/BUILD @@ -0,0 +1,6 @@ +# managed by go/iml_to_build +java_import( + name = "libwebp", + jars = ["libwebp.jar"], + visibility = ["//visibility:public"], +) diff --git a/plugins/webp/lib/libwebp.jar b/plugins/webp/lib/libwebp.jar new file mode 100644 index 000000000000..2fc502b97210 Binary files /dev/null and b/plugins/webp/lib/libwebp.jar differ diff --git a/plugins/webp/lib/libwebp/BUILD b/plugins/webp/lib/libwebp/BUILD new file mode 100644 index 000000000000..59688324a804 --- /dev/null +++ b/plugins/webp/lib/libwebp/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "libwebp", + srcs = glob([ + "mac/*.dylib", + "win/*.dll", + "linux/*.so", + ]), + visibility = ["//visibility:public"], +) diff --git a/plugins/webp/lib/libwebp/README.md b/plugins/webp/lib/libwebp/README.md new file mode 100644 index 000000000000..06477ecc1077 --- /dev/null +++ b/plugins/webp/lib/libwebp/README.md @@ -0,0 +1,95 @@ +# Building webp jni library + +The webp support requires a native library (`libwebp_jni`) and `libwebp.jar`. +The native libraries are added to the `tools/adt/idea/` project in +`adt-ui/lib/libwebp/[platform]`. + +This describes how to build `libwebp_jni` for each of the platforms. The steps +must be repeated for each platform - once for 32 bit and once for 64 bit +architectures. __Make sure to do a clean build when switching architectures__: +`git clean -dfX`. + +`libwebp.jar` is copied directly from `/swig/` to `adt-ui/lib/` + +## Windows: +1. Install Visual Studio 2015 with the following components: + - Visual C++ (including all sub components) + - Universal Windows App Development Tools + - Git for Windows +1. Open "Git Bash", clone libwebp and checkout appropriate branch: + ```bat + git clone https://chromium.googlesource.com/webm/libwebp + ``` +1. Open "Developer Command Prompt for VS2015" +1. Make the webp_jni library as per the following commands: + ```bat + # For 64 bit arch (not needed for 32 bit arch) + cd "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC" + vcvarsall amd64 + + cd \git\clone\location\libwebp + # Make libwebp + nmake /f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output + # Make jni binding now + cd swig + # For 32 bit + cl /I..\src /I"%JDK%\include" ^ + /I"%JDK%\include\win32" ^ + libwebp_java_wrap.c /MT /LD /Fe:webp_jni.dll ^ + ..\output\release-static\x86\lib\libwebp.lib + # For 64 bit + cl /I..\src /I"C:\Program Files (x86)\Java\jdk1.7.0_55\include" ^ + /I"C:\Program Files (x86)\Java\jdk1.7.0_55\include\win32" ^ + libwebp_java_wrap.c /MT /LD /Fe:webp_jni64.dll ^ + ..\output\release-static\x64\lib\libwebp.lib + ``` +1. Note that unlike mac and linux, windows library is named `webp_jni` and not + `libwebp_jni`. + +## Mac/Linux +1. Clone libwebp and checkout appropriate branch: + ```bash + git clone https://chromium.googlesource.com/webm/libwebp + ``` +1. Edit makefile.unix: + ```diff + --- a/makefile.unix + +++ b/makefile.unix + @@ -14,10 +14,11 @@ + # These flags assume you have libpng, libjpeg, libtiff and libgif installed. If + # not, either follow the install instructions below or just comment out the next + # four lines. + -EXTRA_FLAGS= -DWEBP_HAVE_PNG -DWEBP_HAVE_JPEG -DWEBP_HAVE_TIFF + -DWEBP_LIBS= -lpng -lz + -CWEBP_LIBS= $(DWEBP_LIBS) -ljpeg -ltiff + -GIF_LIBS = -lgif + +EXTRA_FLAGS= -fPIC -m32 + +#-DWEBP_HAVE_PNG -DWEBP_HAVE_JPEG -DWEBP_HAVE_TIFF + +#DWEBP_LIBS= -lpng -lz + +#CWEBP_LIBS= $(DWEBP_LIBS) -ljpeg -ltiff + +#GIF_LIBS = -lgif + + ifeq ($(strip $(shell uname)), Darwin) + # Work around a problem linking tables marked as common symbols, + ``` +1. The above diff is for building for 32 bit arch. To build for 64 bit arch, do + not add `-m32` to `EXTRA_FLAGS`. +1. Make the libwebp_jni library as per the following commands + ```bash + cd /git/clone/location/libwebp + make -f makefile.unix + cd swig + gcc -shared -fPIC -fno-strict-aliasing -O2 \ + -m32 \ + -I/path/to/your/jdk/include \ + -I/path/to/your/jdk/include/[platform] \ + -I../src \ + -L../src \ + libwebp_java_wrap.c \ + -lwebp \ + -o libwebp_jni.[ext] + ``` +1. When building for 64 bit, use `-o libwebp_jni64.[ext]` instead and do not + specify the `-m32` option. +1. The `[ext]` for linux is `so` and for mac is `dylib`. +1. No need to build the 32 bit variant for mac. diff --git a/plugins/webp/lib/libwebp/linux/libwebp_jni.so b/plugins/webp/lib/libwebp/linux/libwebp_jni.so new file mode 100755 index 000000000000..501c381cca4f Binary files /dev/null and b/plugins/webp/lib/libwebp/linux/libwebp_jni.so differ diff --git a/plugins/webp/lib/libwebp/linux/libwebp_jni64.so b/plugins/webp/lib/libwebp/linux/libwebp_jni64.so new file mode 100755 index 000000000000..c33a06c73809 Binary files /dev/null and b/plugins/webp/lib/libwebp/linux/libwebp_jni64.so differ diff --git a/plugins/webp/lib/libwebp/mac/libwebp_jni64.dylib b/plugins/webp/lib/libwebp/mac/libwebp_jni64.dylib new file mode 100755 index 000000000000..e127464da47f Binary files /dev/null and b/plugins/webp/lib/libwebp/mac/libwebp_jni64.dylib differ diff --git a/plugins/webp/lib/libwebp/win/webp_jni.dll b/plugins/webp/lib/libwebp/win/webp_jni.dll new file mode 100755 index 000000000000..98af3587ae11 Binary files /dev/null and b/plugins/webp/lib/libwebp/win/webp_jni.dll differ diff --git a/plugins/webp/lib/libwebp/win/webp_jni64.dll b/plugins/webp/lib/libwebp/win/webp_jni64.dll new file mode 100755 index 000000000000..7293adb7fb5e Binary files /dev/null and b/plugins/webp/lib/libwebp/win/webp_jni64.dll differ diff --git a/plugins/webp/lib/licenses/libwebp-NOTICE b/plugins/webp/lib/licenses/libwebp-NOTICE new file mode 100644 index 000000000000..7a6f99547d4d --- /dev/null +++ b/plugins/webp/lib/licenses/libwebp-NOTICE @@ -0,0 +1,30 @@ +Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/plugins/webp/resources/META-INF/plugin.xml b/plugins/webp/resources/META-INF/plugin.xml new file mode 100644 index 000000000000..2751cb925f67 --- /dev/null +++ b/plugins/webp/resources/META-INF/plugin.xml @@ -0,0 +1,13 @@ + + intellij.webp + JetBrains + com.intellij.modules.lang + Webp Support + Provides support for webp images format.

+ ]]>
+ + + + +
\ No newline at end of file diff --git a/plugins/webp/src/com/android/tools/adtui/webp/WebpImageReaderSpi.java b/plugins/webp/src/com/android/tools/adtui/webp/WebpImageReaderSpi.java new file mode 100644 index 000000000000..4e95cd85e82c --- /dev/null +++ b/plugins/webp/src/com/android/tools/adtui/webp/WebpImageReaderSpi.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tools.adtui.webp; + + +import com.google.webp.libwebp; +import com.intellij.openapi.diagnostic.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.util.Iterator; +import java.util.Locale; + +/** + * Decoder for WebP. This needs the webp jni library loaded to function. + * + * @see IIORegistry + */ +public class WebpImageReaderSpi extends ImageReaderSpi { + + private static final byte[] RIFF_HEADER = {'R', 'I', 'F', 'F'}; + private static final byte[] WEBP_HEADER = {'W', 'E', 'B', 'P'}; + + private static final int MAX_FILE_SIZE = 0x6400000; // 100 Megs + + WebpImageReaderSpi() { + vendorName = WebpMetadata.WEBP_VENDOR; + version = WebpNativeLibHelper.getDecoderVersion(); + suffixes = WebpMetadata.WEBP_SUFFIXES; + names = WebpMetadata.WEBP_FORMAT_NAMES; + MIMETypes = WebpMetadata.WEBP_MIME_TYPES; + pluginClassName = WebpReader.class.getName(); + inputTypes = new Class[]{ImageInputStream.class}; + } + + @Override + public boolean canDecodeInput(@NotNull Object source) throws IOException { + assert source instanceof ImageInputStream; + ImageInputStream stream = (ImageInputStream) source; + long length = stream.length(); + // The length may be -1 for files of unknown size. + // Accept them for now and if needed, throw an IOException later. + if (length > MAX_FILE_SIZE) { + return false; + } + + stream.mark(); + try { + byte[] header = new byte[12]; + int bytesRead = stream.read(header, 0, 12); + return bytesRead == 12 && + arrayEquals(header, 0, RIFF_HEADER.length, RIFF_HEADER) && + arrayEquals(header, 8, WEBP_HEADER.length, WEBP_HEADER) && + WebpNativeLibHelper.loadNativeLibraryIfNeeded(); + } finally { + try { + stream.reset(); + } + catch (IOException e) { + Logger.getInstance(WebpImageReaderSpi.class).error(e); + } + } + } + + private static boolean arrayEquals(@NotNull byte[] a1, int offset, int len, @NotNull byte[] a2) { + for (int i = 0; i < len; i++) { + if (a1[offset + i] != a2[i]) { + return false; + } + } + return true; + } + + @NotNull + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + WebpNativeLibHelper.requireNativeLibrary(); + return new WebpReader(this); + } + + @NotNull + @Override + public String getDescription(Locale locale) { + return "Webp Image Decoder"; + } + + private static class WebpReader extends ImageReader { + + private static final String UNABLE_TO_READ_WEBP_IMAGE = "Unable to read WebP image"; + + @Nullable + private byte[] myInputBytes; + private final int[] myWidthOut = new int[1]; + private final int[] myHeightOut = new int[1]; + private int myError; + + public WebpReader(ImageReaderSpi originatingProvider) { + super(originatingProvider); + } + + @Override + public void dispose() { + myInputBytes = null; + } + + @Override + public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { + super.setInput(input, seekForwardOnly, ignoreMetadata); + myInputBytes = null; + myError = 0; + } + + @Override + public int getNumImages(boolean allowSearch) throws IOException { + return myInputBytes == null ? 0 : 1; + } + + private void loadInfoIfNeeded() throws IOException { + if (input != null && myInputBytes == null) { + myInputBytes = readStreamFully((ImageInputStream) input); + myError = libwebp.WebPGetInfo(myInputBytes, myInputBytes.length, myWidthOut, myHeightOut); + } + } + + @NotNull + private static byte[] readStreamFully(@NotNull ImageInputStream stream) throws IOException { + if (stream.length() != -1) { + byte[] bytes = new byte[(int) stream.length()]; // Integer overflow prevented by canDecode check in reader spi above. + + stream.readFully(bytes); + return bytes; + } + + // Unknown file size + ByteArrayOutputStream buffer = new ByteArrayOutputStream(0x100000); // initialize with 1 Meg to minimize reallocs. + final int bufferSize = 0x4000; // 16k + byte[] bytes = new byte[bufferSize]; + int idx; + for (idx = 0; idx < MAX_FILE_SIZE / bufferSize; idx++) { // Just to make sure we don't exceed MAX_FILE_SIZE + int read = stream.read(bytes, 0, bufferSize); + buffer.write(bytes, 0, read); + if (read != bufferSize) { + break; + } + } + if (idx == MAX_FILE_SIZE / bufferSize) { + throw new IOException("webp image too large"); + } + return buffer.toByteArray(); + } + + @Override + public int getWidth(int imageIndex) throws IOException { + loadInfoIfNeeded(); + if (myError != 0) { + // Yes, the above check is correct - zero value means an error. + return myWidthOut[0]; + } + throw new IOException(UNABLE_TO_READ_WEBP_IMAGE); + } + + @Override + public int getHeight(int imageIndex) throws IOException { + loadInfoIfNeeded(); + if (myError != 0) { + // Yes, the above check is correct - zero value means an error. + return myHeightOut[0]; + } + throw new IOException(UNABLE_TO_READ_WEBP_IMAGE); + } + + @Nullable + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + return null; + } + + @Nullable + @Override + public IIOMetadata getStreamMetadata() throws IOException { + return null; + } + + @Nullable + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + return null; + } + + @NotNull + @Override + public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { + loadInfoIfNeeded(); + if (myError == 0) { + throw new IOException(UNABLE_TO_READ_WEBP_IMAGE); + } + assert myInputBytes != null; + byte[] argb = libwebp.WebPDecodeARGB(myInputBytes, myInputBytes.length, myWidthOut, myHeightOut); + @SuppressWarnings("UndesirableClassUsage") + BufferedImage bi = new BufferedImage(myWidthOut[0], myHeightOut[0], BufferedImage.TYPE_INT_ARGB); + // Copy the bytes read above to the image's data buffer. + final int[] a = ((DataBufferInt) bi.getRaster().getDataBuffer()).getData(); + IntBuffer intBuf = ByteBuffer.wrap(argb).asIntBuffer(); + assert a.length == intBuf.remaining(); + intBuf.get(a); + return bi; + } + } +} diff --git a/plugins/webp/src/com/android/tools/adtui/webp/WebpImageWriteParam.java b/plugins/webp/src/com/android/tools/adtui/webp/WebpImageWriteParam.java new file mode 100644 index 000000000000..eb4c7389a242 --- /dev/null +++ b/plugins/webp/src/com/android/tools/adtui/webp/WebpImageWriteParam.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tools.adtui.webp; + +import javax.imageio.ImageWriteParam; +import java.util.Locale; + +class WebpImageWriteParam extends ImageWriteParam { + WebpImageWriteParam(Locale locale, boolean compress, float quality) { + super(locale); + this.canWriteCompressed = compress; + this.compressionQuality = quality; + if (canWriteCompressed) { + compressionMode = ImageWriteParam.MODE_EXPLICIT; + } + } +} \ No newline at end of file diff --git a/plugins/webp/src/com/android/tools/adtui/webp/WebpImageWriterSpi.java b/plugins/webp/src/com/android/tools/adtui/webp/WebpImageWriterSpi.java new file mode 100644 index 000000000000..59467a1a6fe3 --- /dev/null +++ b/plugins/webp/src/com/android/tools/adtui/webp/WebpImageWriterSpi.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tools.adtui.webp; + + +import com.google.webp.libwebp; +import org.jetbrains.annotations.NotNull; + +import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.ImageOutputStream; +import java.awt.*; +import java.awt.image.*; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.util.Iterator; +import java.util.Locale; + +/** + * Encoder for WebP. This needs the webp jni library loaded to function. + */ +public class WebpImageWriterSpi extends ImageWriterSpi { + WebpImageWriterSpi() { + vendorName = WebpMetadata.WEBP_VENDOR; + version = WebpNativeLibHelper.getEncoderVersion(); + suffixes = WebpMetadata.WEBP_SUFFIXES; + names = WebpMetadata.WEBP_FORMAT_NAMES; + MIMETypes = WebpMetadata.WEBP_MIME_TYPES; + pluginClassName = WebpWriter.class.getName(); + outputTypes = new Class[]{ImageOutputStream.class}; + } + + public static void writeImage(@NotNull BufferedImage image, + @NotNull OutputStream outputStream, + boolean lossless, + int quality) throws IOException { + // Instead of + // ImageIO.write(image, WebpMetadata.WEBP_FORMAT_LOWER_CASE, stream); + // which will pass null as the parameters to the ImageWriter, call through to the ImageWriter + // more directly so we can set up the proper parameters: + ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(image); + Iterator iterator = ImageIO.getImageWriters(type, WebpMetadata.WEBP_FORMAT_LOWER_CASE); + if (iterator.hasNext()) { + ImageWriter writer = iterator.next(); + try (ImageOutputStream stream = ImageIO.createImageOutputStream(outputStream)) { + writer.setOutput(stream); + try { + ImageWriteParam param = new WebpImageWriteParam(writer.getLocale(), !lossless, quality / 100f); + writer.write(null, new IIOImage(image, null, null), param); + } + finally { + writer.dispose(); + stream.flush(); + } + } + } + } + + @Override + public boolean canEncodeImage(ImageTypeSpecifier type) { + return canWriteImage(type); + } + + public static boolean canWriteImage(ImageTypeSpecifier type) { + SampleModel sm = type.getSampleModel(); + ColorModel cm = type.getColorModel(); + return (sm.getNumBands() >= 2 && sm.getNumBands() <= 4 || cm instanceof IndexColorModel) && + sm.getSampleSize(0) <= 8 && + sm.getWidth() <= 65535 && + sm.getHeight() <= 65535 && + (cm == null || cm.getComponentSize()[0] <= 8) && + WebpNativeLibHelper.loadNativeLibraryIfNeeded(); + } + + @Override + public ImageWriter createWriterInstance(Object extension) throws IOException { + WebpNativeLibHelper.requireNativeLibrary(); + return new WebpWriter(this); + } + + @Override + public String getDescription(Locale locale) { + return "WebP Image Encoder"; + } + + private static class WebpWriter extends ImageWriter { + public WebpWriter(ImageWriterSpi originatingProvider) { + super(originatingProvider); + } + + @Override + public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { + return new WebpMetadata(); + } + + @Override + public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { + return new WebpMetadata(); + } + + @Override + public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) { + return null; + } + + @Override + public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { + return null; + } + + private ImageOutputStream stream = null; + + @Override + public void setOutput(Object output) { + super.setOutput(output); + if (output != null) { + if (!(output instanceof ImageOutputStream)) { + throw new + IllegalArgumentException("output is not an ImageOutputStream"); + } + this.stream = (ImageOutputStream)output; + this.stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + else { + this.stream = null; + } + } + + + @Override + public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException { + if (stream == null) { + throw new IllegalStateException("output == null!"); + } + boolean lossless = WebpMetadata.DEFAULT_LOSSLESS; + float quality = WebpMetadata.DEFAULT_ENCODING_QUALITY; + if (param != null && param.canWriteCompressed()) { + lossless = false; + if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) { + quality = param.getCompressionQuality(); + } + } + + Raster srcRas = getRaster(image); + + int minX = srcRas.getMinX(); + int minY = srcRas.getMinY(); + int width = srcRas.getWidth(); + int height = srcRas.getHeight(); + int maxX = minX + width; + int maxY = minY + height; + + byte[] data = new byte[width * height * 4]; + // TODO: This code is the inefficient fallback dealing with any color/sample model. + // However, we've restricted things in the canEncodeImage method so we might as well try + // to do it more efficiently + int index = 0; + SampleModel sampleModel = srcRas.getSampleModel(); + int bands = sampleModel.getNumBands(); + if (bands == 1) { + // Indexed image but not found in raster + return; + } + assert bands >= 2 && bands <= 4; // enforced in canEncodeImage + DataBuffer buffer = srcRas.getDataBuffer(); + if (bands >= 3) { + for (int y = minY; y < maxY; y++) { + for (int x = minX; x < maxX; x++) { + for (int band = 0; band < bands; band++) { + int sample = sampleModel.getSample(x, y, band, buffer); + data[index++] = (byte)sample; + } + if (bands == 3) { + data[index++] = (byte)255; // implicit alpha in an RGB (not RGBA) image + } + } + } + } + else { + assert bands == 2; + for (int y = minY; y < maxY; y++) { + for (int x = minX; x < maxX; x++) { + byte sample = (byte)sampleModel.getSample(x, y, 0, buffer); + data[index++] = sample; + data[index++] = sample; + data[index++] = sample; + byte alpha = (byte)sampleModel.getSample(x, y, 1, buffer); + data[index++] = alpha; + } + } + } + + // See https://developers.google.com/speed/webp/docs/api + byte[] encoded; + if (lossless) { + encoded = libwebp.WebPEncodeLosslessRGBA(data, width, height, 4 * width); + } + else { + encoded = libwebp.WebPEncodeRGBA(data, width, height, 4 * width, (int)(100 * quality)); + } + stream.write(encoded); + } + + @NotNull + private static Raster getRaster(@NotNull IIOImage image) { + boolean rasterOnly = image.hasRaster(); + if (rasterOnly) { + return image.getRaster(); + } + else { + RenderedImage renderedImage = image.getRenderedImage(); + if (renderedImage instanceof BufferedImage) { + // Convert indexed to RGB + BufferedImage bufferedImage = (BufferedImage)renderedImage; + if (renderedImage.getColorModel() instanceof IndexColorModel) { + //noinspection UndesirableClassUsage + BufferedImage rgb = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = rgb.createGraphics(); + graphics.drawImage(bufferedImage, 0, 0, null); + graphics.dispose(); + return rgb.getRaster(); + } + return bufferedImage.getRaster(); + } + else if (renderedImage.getNumXTiles() == 1 && + renderedImage.getNumYTiles() == 1) { + Raster raster = renderedImage.getTile(renderedImage.getMinTileX(), + renderedImage.getMinTileY()); + if (raster.getWidth() != renderedImage.getWidth() || + raster.getHeight() != renderedImage.getHeight()) { + raster = raster.createChild(raster.getMinX(), + raster.getMinY(), + renderedImage.getWidth(), + renderedImage.getHeight(), + raster.getMinX(), + raster.getMinY(), + null); + } + return raster; + } + else { + return renderedImage.getData(); + } + } + } + } +} diff --git a/plugins/webp/src/com/android/tools/adtui/webp/WebpMetadata.java b/plugins/webp/src/com/android/tools/adtui/webp/WebpMetadata.java new file mode 100644 index 000000000000..c98c62b9b9f6 --- /dev/null +++ b/plugins/webp/src/com/android/tools/adtui/webp/WebpMetadata.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tools.adtui.webp; + +import com.intellij.ide.ApplicationInitializedListener; +import org.w3c.dom.Node; + +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ImageWriterSpi; + +public class WebpMetadata extends IIOMetadata implements ApplicationInitializedListener { + public static final String WEBP_FORMAT_LOWER_CASE = "webp"; + public static final String WEBP_FORMAT_UPPER_CASE = "WEBP"; + public static final String[] WEBP_FORMAT_NAMES = new String[] {WEBP_FORMAT_UPPER_CASE, WEBP_FORMAT_LOWER_CASE}; + public static final String EXT_WEBP = WEBP_FORMAT_LOWER_CASE; + public static final String[] WEBP_SUFFIXES = new String[] { EXT_WEBP}; + public static final String[] WEBP_MIME_TYPES = new String[] {"image/webp"}; + public static final String WEBP_VENDOR = "Google LLC"; + public static final float DEFAULT_ENCODING_QUALITY = 0.75f; + public static final boolean DEFAULT_LOSSLESS = true; + + @Override + public void componentsInitialized() { + ensureWebpRegistered(); + } + + /** + * Ensures that service providers are registered. + */ + public static void ensureWebpRegistered() { + IIORegistry.getDefaultInstance().registerServiceProvider(new WebpImageReaderSpi(), ImageReaderSpi.class); + IIORegistry.getDefaultInstance().registerServiceProvider(new WebpImageWriterSpi(), ImageWriterSpi.class); + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public Node getAsTree(String formatName) { + return new IIOMetadataNode(nativeMetadataFormatName); + } + + @Override + public void mergeTree(String formatName, Node root) throws IIOInvalidTreeException { + } + + @Override + public void reset() { + } +} diff --git a/plugins/webp/src/com/android/tools/adtui/webp/WebpNativeLibHelper.java b/plugins/webp/src/com/android/tools/adtui/webp/WebpNativeLibHelper.java new file mode 100644 index 000000000000..6e2c676c2d2e --- /dev/null +++ b/plugins/webp/src/com/android/tools/adtui/webp/WebpNativeLibHelper.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tools.adtui.webp; + +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.application.PluginPathManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.SystemInfo; + +import java.io.File; +import java.io.IOException; + +public class WebpNativeLibHelper { + + @SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized") private static boolean sJniLibLoaded; + @SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized") private static boolean sJniLibLoadAttempted; + + private WebpNativeLibHelper() { + } + + static void requireNativeLibrary() throws IOException { + if (!loadNativeLibraryIfNeeded()) { + throw new IOException("The WebP decoder library could not be loaded"); + } + } + + public static String getDecoderVersion() { + // This is the current result of calling + // libwebp.WebPGetEncoderVersion() + // but we don't want to have to load the native library just to get this constant + // (since it doesn't change until we change the bundled version of the library anyway). + // + // And more importantly, NOBODY looks at this version anyway. + return "1280"; + } + + public static String getEncoderVersion() { + return getDecoderVersion(); + } + + public static boolean loadNativeLibraryIfNeeded() { + if (!sJniLibLoadAttempted) { + try { + loadNativeLibrary(); + } + catch (UnsatisfiedLinkError e) { + Logger.getInstance(WebpImageReaderSpi.class).warn(e); + } + } + return sJniLibLoaded; + } + + private synchronized static void loadNativeLibrary() { + if (sJniLibLoadAttempted) { + // Already attempted to load, nothing to do here. + return; + } + try { + String libFileName = getLibName(); + File pluginPath = getLibLocation(); + File libPath = new File(pluginPath, libFileName); + if (!libPath.exists()) { + throw new UnsatisfiedLinkError(String.format("'%1$s' not found at '%2$s'", libFileName, libPath.getAbsolutePath())); + } + System.load(libPath.getAbsolutePath()); + } + finally { + sJniLibLoadAttempted = true; + } + sJniLibLoaded = true; + } + + /** + * Copied from {@link com.intellij.util.lang.UrlClassLoader}. + */ + public static String getLibName() { + String baseName = SystemInfo.is64Bit ? "webp_jni64" : "webp_jni"; + String fileName = System.mapLibraryName(baseName); + if (SystemInfo.isMac) { + fileName = fileName.replace(".jnilib", ".dylib"); + } + return fileName; + } + + public static File getLibLocation() { + // A terrible hack for dev environment. + String libPath = "/lib/libwebp/" + getPlatformName(); + return new File(PluginPathManager.getPluginHomePath("webp") + libPath); + } + + private static String getPlatformName() { + if (SystemInfo.isWindows) return "win"; + else if (SystemInfo.isMac) return "mac"; + else if (SystemInfo.isLinux) return "linux"; + else return ""; + } +}