diff --git a/gapic/src/main/com/google/gapid/image/ArrayImage.java b/gapic/src/main/com/google/gapid/image/ArrayImage.java index 152f341b9f..7bb7fea4ed 100644 --- a/gapic/src/main/com/google/gapid/image/ArrayImage.java +++ b/gapic/src/main/com/google/gapid/image/ArrayImage.java @@ -19,6 +19,7 @@ import static com.google.gapid.util.Colors.DARK_LUMINANCE_THRESHOLD; import static com.google.gapid.util.Colors.clamp; +import com.google.common.primitives.UnsignedBytes; import com.google.gapid.glviewer.gl.Texture; import com.google.gapid.util.Colors; @@ -168,8 +169,11 @@ public Builder flip() { * An {@link ArrayImage} that represents an RGBA image with 8bit color channels. */ public static class RGBA8Image extends ArrayImage { + private final PixelInfo info; + public RGBA8Image(int width, int height, int depth, byte[] data) { super(width, height, depth, 4, data, GL11.GL_RGBA8, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE); + this.info = IntPixelInfo.compute(data); } @Override @@ -202,7 +206,7 @@ protected PixelValue getPixel(int x, int y, byte[] data) { @Override public PixelInfo getInfo() { - return PixelInfo.NULL_INFO; + return info; } private static class Pixel implements PixelValue { @@ -420,10 +424,13 @@ public boolean isDark() { private static class FloatPixelInfo implements PixelInfo { private final float min, max; + private final float alphaMin, alphaMax; - private FloatPixelInfo(float min, float max) { + private FloatPixelInfo(float min, float max, float alphaMin, float alphaMax) { this.min = min; this.max = max; + this.alphaMin = alphaMin; + this.alphaMax = alphaMax; } public static PixelInfo compute(FloatBuffer buffer, boolean isRGBA) { @@ -432,7 +439,10 @@ public static PixelInfo compute(FloatBuffer buffer, boolean isRGBA) { } float min = Float.POSITIVE_INFINITY, max = Float.NEGATIVE_INFINITY; + float alphaMin, alphaMax; if (isRGBA) { + alphaMin = Float.POSITIVE_INFINITY; + alphaMax = Float.NEGATIVE_INFINITY; for (int i = 0, end = buffer.remaining() - 3; i <= end; ) { float value = buffer.get(i++); if (!Float.isNaN(value) && !Float.isInfinite(value)) { @@ -449,9 +459,14 @@ public static PixelInfo compute(FloatBuffer buffer, boolean isRGBA) { min = Math.min(min, value); max = Math.max(max, value); } - i++; // skip alpha + value = buffer.get(i++); + if (!Float.isNaN(value) && !Float.isInfinite(value)) { + alphaMin = Math.min(alphaMin, value); + alphaMax = Math.max(alphaMax, value); + } } } else { + alphaMin = alphaMax = 1; for (int i = 0; i < buffer.remaining(); i++) { float value = buffer.get(i); if (!Float.isNaN(value) && !Float.isInfinite(value)) { @@ -460,7 +475,7 @@ public static PixelInfo compute(FloatBuffer buffer, boolean isRGBA) { } } } - return new FloatPixelInfo(min, max); + return new FloatPixelInfo(min, max, alphaMin, alphaMax); } @Override @@ -472,5 +487,58 @@ public float getMin() { public float getMax() { return max; } + + @Override + public float getAlphaMin() { + return alphaMin; + } + + @Override + public float getAlphaMax() { + return alphaMax; + } + } + + private static class IntPixelInfo implements PixelInfo { + private final float alphaMin, alphaMax; + + private IntPixelInfo(float alphaMin, float alphaMax) { + this.alphaMin = alphaMin; + this.alphaMax = alphaMax; + } + + public static PixelInfo compute(byte[] rgba) { + if (rgba.length == 0) { + return PixelInfo.NULL_INFO; + } + + int alphaMin = Integer.MAX_VALUE, alphaMax = Integer.MIN_VALUE; + for (int i = 3; i < rgba.length; i += 4) { + int value = UnsignedBytes.toInt(rgba[i]); + alphaMin = Math.min(alphaMin, value); + alphaMax = Math.max(alphaMax, value); + } + return new IntPixelInfo(alphaMin / 255f, alphaMax / 255f); + } + + @Override + public float getMin() { + return 0; // Disable automatic tone-mapping. + } + + @Override + public float getMax() { + return 1; // Disable automatic tone-mapping. + } + + @Override + public float getAlphaMin() { + return alphaMin; + } + + @Override + public float getAlphaMax() { + return alphaMax; + } } } diff --git a/gapic/src/main/com/google/gapid/image/Image.java b/gapic/src/main/com/google/gapid/image/Image.java index 4d832cb549..aa47cd5662 100644 --- a/gapic/src/main/com/google/gapid/image/Image.java +++ b/gapic/src/main/com/google/gapid/image/Image.java @@ -153,6 +153,16 @@ public float getMin() { public float getMax() { return 1; } + + @Override + public float getAlphaMin() { + return 1; + } + + @Override + public float getAlphaMax() { + return 1; + } }; /** @@ -164,5 +174,15 @@ public float getMax() { * @return the maximum value across all channels of the image data. Used for tone mapping. */ public float getMax(); + + /** + * @return the minimum alpha value of the image data. + */ + public float getAlphaMin(); + + /** + * @return the maximum alpha value of the image data. + */ + public float getAlphaMax(); } } diff --git a/gapic/src/main/com/google/gapid/widgets/ImagePanel.java b/gapic/src/main/com/google/gapid/widgets/ImagePanel.java index e673de19e4..8ee91582fc 100644 --- a/gapic/src/main/com/google/gapid/widgets/ImagePanel.java +++ b/gapic/src/main/com/google/gapid/widgets/ImagePanel.java @@ -24,6 +24,7 @@ import static com.google.gapid.widgets.Widgets.createSeparator; import static com.google.gapid.widgets.Widgets.createToggleToolItem; import static com.google.gapid.widgets.Widgets.createToolItem; +import static com.google.gapid.widgets.Widgets.withSpans; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -84,6 +85,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; import java.util.function.IntConsumer; import java.util.logging.Logger; @@ -94,6 +96,7 @@ public class ImagePanel extends Composite { protected static final Logger LOG = Logger.getLogger(ImagePanel.class.getName()); protected static final int ZOOM_AMOUNT = 5; private static final int CHANNEL_RED = 0, CHANNEL_GREEN = 1, CHANNEL_BLUE = 2, CHANNEL_ALPHA = 3; + private static final float ALPHA_WARNING_THRESHOLD = 2 / 255f; private static final Image[] NO_LAYERS = new Image[] { Image.EMPTY }; private final SingleInFlight imageRequestController = new SingleInFlight(); @@ -112,8 +115,8 @@ public ImagePanel(Composite parent, Widgets widgets, boolean naturallyFlipped) { setLayout(Widgets.withMargin(new GridLayout(1, false), 5, 2)); loading = LoadablePanel.create(this, widgets, panel -> - new ImageComponent(panel, widgets.theme, naturallyFlipped)); - status = new StatusBar(this, this::loadLevel); + new ImageComponent(panel, widgets.theme, this::showAlphaWarning, naturallyFlipped)); + status = new StatusBar(this, widgets.theme, this::loadLevel, this::setAlphaEnabled); imageComponent = loading.getContents(); loading.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); @@ -185,6 +188,14 @@ protected void setPreviewPixel(Pixel pixel) { status.setPixel(pixel); } + private void showAlphaWarning(AlphaWarning message) { + status.showAlphaWarning(message); + } + + private void setAlphaEnabled(boolean enabled) { + imageComponent.autoToggleAlphaChannel(enabled); + } + public void createToolbar(ToolBar bar, Theme theme) { zoomFitItem = createToggleToolItem(bar, theme.zoomFit(), e -> setZoomToFit(((ToolItem)e.widget).getSelection()), "Zoom to fit"); @@ -400,6 +411,7 @@ private static class ImageComponent extends Composite { private static final double MAX_ZOOM_FACTOR = 8; private static final VecD MIN_ZOOM_SIZE = new VecD(100, 100, 0); + private final Consumer showAlphaWarning; private final boolean naturallyFlipped; private final ScrollBar scrollbars[]; @@ -422,10 +434,14 @@ private static class ImageComponent extends Composite { private double scaleGridToView = 1.0; private boolean zoomToFit; - public ImageComponent(Composite parent, Theme theme, boolean naturallyFlipped) { + private boolean alphaWasAutoDisabled = false; + + public ImageComponent(Composite parent, Theme theme, Consumer showAlphaWarning, + boolean naturallyFlipped) { super(parent, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.NO_BACKGROUND); setLayout(new FillLayout(SWT.VERTICAL)); + this.showAlphaWarning = showAlphaWarning; this.naturallyFlipped = naturallyFlipped; scrollbars = new ScrollBar[] { getHorizontalBar(), getVerticalBar() }; @@ -492,6 +508,33 @@ private void refresh() { data.images = images; data.transforms = calcTransforms(); canvas.setSceneData(data.copy()); + + if (images.length == 0) { + showAlphaWarning.accept(AlphaWarning.NONE); + } else if (isChannelEnabled(CHANNEL_ALPHA)) { + boolean noAlpha = true; + for (Image image : images) { + if (image.getInfo().getAlphaMax() > ALPHA_WARNING_THRESHOLD) { + noAlpha = false; + break; + } + } + showAlphaWarning.accept(noAlpha ? AlphaWarning.NO_ALPHA : AlphaWarning.NONE); + } else if (alphaWasAutoDisabled) { + boolean noAlpha = true; + for (Image image : images) { + PixelInfo info = image.getInfo(); + if (info.getAlphaMax() > ALPHA_WARNING_THRESHOLD && + info.getAlphaMin() < 1 - ALPHA_WARNING_THRESHOLD) { + // Consider an image with all alpha values mostly 1.0 as an image without alpha. + noAlpha = false; + break; + } + } + showAlphaWarning.accept(noAlpha ? AlphaWarning.NONE : AlphaWarning.ALPHA_DISABLED); + } else { + showAlphaWarning.accept(AlphaWarning.NONE); + } } public void setPreviewPixel(Pixel previewPixel) { @@ -524,6 +567,15 @@ protected boolean isChannelEnabled(int channel) { protected void setChannelEnabled(int channel, boolean enabled) { data.channels[channel] = enabled; + if (channel == CHANNEL_ALPHA) { + alphaWasAutoDisabled = false; + } + refresh(); + } + + protected void autoToggleAlphaChannel(boolean enabled) { + alphaWasAutoDisabled = true; + data.channels[CHANNEL_ALPHA] = enabled; refresh(); } @@ -709,7 +761,6 @@ private static class ImageScene implements Scene { private static final int PREVIEW_HEIGHT = 11; // Should be odd, so center pixel looks nice. private static final int PREVIEW_SIZE = 7; - private final Map imageToTexture = Maps.newHashMap(); private Shader shader; private Texture[] textures; @@ -964,9 +1015,12 @@ private static class StatusBar extends Composite { private final Label levelValue; private final Label levelSize; private final Label pixelLabel; + private final Label warning; private int lastSelection = 0; + private AlphaWarning lastWarning = AlphaWarning.NONE; - public StatusBar(Composite parent, IntConsumer levelListener) { + public StatusBar( + Composite parent, Theme theme, IntConsumer levelListener, Consumer enableAlpha) { super(parent, SWT.NONE); setLayout(new GridLayout(3, false)); @@ -976,10 +1030,15 @@ public StatusBar(Composite parent, IntConsumer levelListener) { levelScale = createScale(levelComposite); levelSize = createLabel(this, ""); pixelLabel = createLabel(this, ""); + warning = createLabel(this, ""); + warning.setForeground(theme.imageWarning()); + warning.setCursor(getDisplay().getSystemCursor(SWT.CURSOR_HAND)); levelComposite.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, true)); levelSize.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, true)); pixelLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); + warning.setLayoutData(withSpans(new GridData(SWT.CENTER, SWT.CENTER, true, false), 3, 1)); + showAlphaWarning(AlphaWarning.NONE); levelScale.addListener(SWT.Selection, e -> { int selection = levelScale.getSelection(); @@ -990,6 +1049,9 @@ public StatusBar(Composite parent, IntConsumer levelListener) { levelListener.accept(selection); } }); + warning.addListener(SWT.MouseUp, + e -> enableAlpha.accept(lastWarning == AlphaWarning.ALPHA_DISABLED)); + setLevelCount(0); } @@ -1019,6 +1081,18 @@ public void setPixel(Pixel pixel) { requestLayout(); } + public void showAlphaWarning(AlphaWarning message) { + if (lastWarning == message) { + return; + } + + lastWarning = message; + ((GridData)warning.getLayoutData()).exclude = message == AlphaWarning.NONE; + warning.setVisible(message != AlphaWarning.NONE); + warning.setText(message.warning); + requestLayout(); + } + private static Scale createScale(Composite parent) { Scale scale = new Scale(parent, SWT.HORIZONTAL); scale.setMinimum(0); @@ -1029,4 +1103,16 @@ private static Scale createScale(Composite parent) { return scale; } } + + private static enum AlphaWarning { + NONE(""), + NO_ALPHA("The alpha channels appears to be empty. Click to disable the alpha channel."), + ALPHA_DISABLED("Image contains an alpha channel. Click to re-enable alpha channel."); + + public final String warning; + + private AlphaWarning(String warning) { + this.warning = warning; + } + } } diff --git a/gapic/src/main/com/google/gapid/widgets/Theme.java b/gapic/src/main/com/google/gapid/widgets/Theme.java index 753552fafc..cba9be6187 100644 --- a/gapic/src/main/com/google/gapid/widgets/Theme.java +++ b/gapic/src/main/com/google/gapid/widgets/Theme.java @@ -138,6 +138,7 @@ public interface Theme { @RGB(argb = 0xffffffff) public Color imageCheckerLight(); @RGB(argb = 0xff000000) public Color imageCursorDark(); @RGB(argb = 0xffffffff) public Color imageCursorLight(); + @RGB(argb = 0xffff9900) public Color imageWarning(); @TextStyle(foreground = 0xa9a9a9) public Styler structureStyler(); @TextStyle(foreground = 0x0000ee) public Styler identifierStyler();