• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.draw9patch.ui;
18 
19 import com.android.draw9patch.graphics.GraphicsUtilities;
20 
21 import javax.swing.JPanel;
22 import javax.swing.JLabel;
23 import javax.swing.BorderFactory;
24 import javax.swing.JSlider;
25 import javax.swing.JComponent;
26 import javax.swing.JScrollPane;
27 import javax.swing.JCheckBox;
28 import javax.swing.Box;
29 import javax.swing.JFileChooser;
30 import javax.swing.JSplitPane;
31 import javax.swing.JButton;
32 import javax.swing.border.EmptyBorder;
33 import javax.swing.event.AncestorEvent;
34 import javax.swing.event.AncestorListener;
35 import javax.swing.event.ChangeListener;
36 import javax.swing.event.ChangeEvent;
37 import java.awt.image.BufferedImage;
38 import java.awt.image.RenderedImage;
39 import java.awt.Graphics2D;
40 import java.awt.BorderLayout;
41 import java.awt.Color;
42 import java.awt.Graphics;
43 import java.awt.Dimension;
44 import java.awt.TexturePaint;
45 import java.awt.Shape;
46 import java.awt.BasicStroke;
47 import java.awt.RenderingHints;
48 import java.awt.Rectangle;
49 import java.awt.GridBagLayout;
50 import java.awt.GridBagConstraints;
51 import java.awt.Insets;
52 import java.awt.Toolkit;
53 import java.awt.AWTEvent;
54 import java.awt.event.MouseMotionAdapter;
55 import java.awt.event.MouseEvent;
56 import java.awt.event.MouseAdapter;
57 import java.awt.event.ActionListener;
58 import java.awt.event.ActionEvent;
59 import java.awt.event.KeyEvent;
60 import java.awt.event.AWTEventListener;
61 import java.awt.geom.Rectangle2D;
62 import java.awt.geom.Line2D;
63 import java.awt.geom.Area;
64 import java.awt.geom.RoundRectangle2D;
65 import java.io.IOException;
66 import java.io.File;
67 import java.net.URL;
68 import java.util.List;
69 import java.util.ArrayList;
70 import java.util.Arrays;
71 
72 class ImageEditorPanel extends JPanel {
73     private static final String EXTENSION_9PATCH = ".9.png";
74     private static final int DEFAULT_ZOOM = 8;
75     private static final float DEFAULT_SCALE = 2.0f;
76 
77     private String name;
78     private BufferedImage image;
79     private boolean is9Patch;
80 
81     private ImageViewer viewer;
82     private StretchesViewer stretchesViewer;
83     private JLabel xLabel;
84     private JLabel yLabel;
85 
86     private TexturePaint texture;
87 
88     private List<Rectangle> patches;
89     private List<Rectangle> horizontalPatches;
90     private List<Rectangle> verticalPatches;
91     private List<Rectangle> fixed;
92     private boolean verticalStartWithPatch;
93     private boolean horizontalStartWithPatch;
94 
95     private Pair<Integer> horizontalPadding;
96     private Pair<Integer> verticalPadding;
97 
ImageEditorPanel(MainFrame mainFrame, BufferedImage image, String name)98     ImageEditorPanel(MainFrame mainFrame, BufferedImage image, String name) {
99         this.image = image;
100         this.name = name;
101 
102         setTransferHandler(new ImageTransferHandler(mainFrame));
103 
104         checkImage();
105 
106         setOpaque(false);
107         setLayout(new BorderLayout());
108 
109         loadSupport();
110         buildImageViewer();
111         buildStatusPanel();
112     }
113 
loadSupport()114     private void loadSupport() {
115         try {
116             URL resource = getClass().getResource("/images/checker.png");
117             BufferedImage checker = GraphicsUtilities.loadCompatibleImage(resource);
118             texture = new TexturePaint(checker, new Rectangle2D.Double(0, 0,
119                     checker.getWidth(), checker.getHeight()));
120         } catch (IOException e) {
121             e.printStackTrace();
122         }
123     }
124 
buildImageViewer()125     private void buildImageViewer() {
126         viewer = new ImageViewer();
127 
128         JSplitPane splitter = new JSplitPane();
129         splitter.setContinuousLayout(true);
130         splitter.setResizeWeight(0.8);
131         splitter.setBorder(null);
132 
133         JScrollPane scroller = new JScrollPane(viewer);
134         scroller.setOpaque(false);
135         scroller.setBorder(null);
136         scroller.getViewport().setBorder(null);
137         scroller.getViewport().setOpaque(false);
138 
139         splitter.setLeftComponent(scroller);
140         splitter.setRightComponent(buildStretchesViewer());
141 
142         add(splitter);
143     }
144 
buildStretchesViewer()145     private JComponent buildStretchesViewer() {
146         stretchesViewer = new StretchesViewer();
147         JScrollPane scroller = new JScrollPane(stretchesViewer);
148         scroller.setBorder(null);
149         scroller.getViewport().setBorder(null);
150         scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
151         scroller.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
152         return scroller;
153     }
154 
buildStatusPanel()155     private void buildStatusPanel() {
156         JPanel status = new JPanel(new GridBagLayout());
157         status.setOpaque(false);
158 
159         JLabel label = new JLabel();
160         label.setForeground(Color.WHITE);
161         label.setText("Zoom: ");
162         label.putClientProperty("JComponent.sizeVariant", "small");
163         status.add(label, new GridBagConstraints(0, 0, 1, 1, 0.0f, 0.0f,
164                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
165                 new Insets(0, 6, 0, 0), 0, 0));
166 
167         label = new JLabel();
168         label.setForeground(Color.WHITE);
169         label.setText("100%");
170         label.putClientProperty("JComponent.sizeVariant", "small");
171         status.add(label, new GridBagConstraints(1, 0, 1, 1, 0.0f, 0.0f,
172                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
173                 new Insets(0, 0, 0, 0), 0, 0));
174 
175         JSlider zoomSlider = new JSlider(1, 16, DEFAULT_ZOOM);
176         zoomSlider.setSnapToTicks(true);
177         zoomSlider.putClientProperty("JComponent.sizeVariant", "small");
178         zoomSlider.addChangeListener(new ChangeListener() {
179             public void stateChanged(ChangeEvent evt) {
180                 viewer.setZoom(((JSlider) evt.getSource()).getValue());
181             }
182         });
183         status.add(zoomSlider, new GridBagConstraints(2, 0, 1, 1, 0.0f, 0.0f,
184                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
185                 new Insets(0, 0, 0, 0), 0, 0));
186 
187         JLabel maxZoomLabel = new JLabel();
188         maxZoomLabel.setForeground(Color.WHITE);
189         maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small");
190         maxZoomLabel.setText("800%");
191         status.add(maxZoomLabel, new GridBagConstraints(3, 0, 1, 1, 0.0f, 0.0f,
192                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
193                 new Insets(0, 0, 0, 0), 0, 0));
194 
195         label = new JLabel();
196         label.setForeground(Color.WHITE);
197         label.setText("Patch scale: ");
198         label.putClientProperty("JComponent.sizeVariant", "small");
199         status.add(label, new GridBagConstraints(0, 1, 1, 1, 0.0f, 0.0f,
200                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
201                 new Insets(0, 6, 0, 0), 0, 0));
202 
203         label = new JLabel();
204         label.setForeground(Color.WHITE);
205         label.setText("2x");
206         label.putClientProperty("JComponent.sizeVariant", "small");
207         status.add(label, new GridBagConstraints(1, 1, 1, 1, 0.0f, 0.0f,
208                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
209                 new Insets(0, 0, 0, 0), 0, 0));
210 
211         zoomSlider = new JSlider(200, 600, (int) (DEFAULT_SCALE * 100.0f));
212         zoomSlider.setSnapToTicks(true);
213         zoomSlider.putClientProperty("JComponent.sizeVariant", "small");
214         zoomSlider.addChangeListener(new ChangeListener() {
215             public void stateChanged(ChangeEvent evt) {
216                 stretchesViewer.setScale(((JSlider) evt.getSource()).getValue() / 100.0f);
217             }
218         });
219         status.add(zoomSlider, new GridBagConstraints(2, 1, 1, 1, 0.0f, 0.0f,
220                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
221                 new Insets(0, 0, 0, 0), 0, 0));
222 
223         maxZoomLabel = new JLabel();
224         maxZoomLabel.setForeground(Color.WHITE);
225         maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small");
226         maxZoomLabel.setText("6x");
227         status.add(maxZoomLabel, new GridBagConstraints(3, 1, 1, 1, 0.0f, 0.0f,
228                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
229                 new Insets(0, 0, 0, 0), 0, 0));
230 
231         JCheckBox showLock = new JCheckBox("Show lock");
232         showLock.setOpaque(false);
233         showLock.setForeground(Color.WHITE);
234         showLock.setSelected(true);
235         showLock.putClientProperty("JComponent.sizeVariant", "small");
236         showLock.addActionListener(new ActionListener() {
237             public void actionPerformed(ActionEvent event) {
238                 viewer.setLockVisible(((JCheckBox) event.getSource()).isSelected());
239             }
240         });
241         status.add(showLock, new GridBagConstraints(4, 0, 1, 1, 0.0f, 0.0f,
242                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
243                 new Insets(0, 12, 0, 0), 0, 0));
244 
245         JCheckBox showPatches = new JCheckBox("Show patches");
246         showPatches.setOpaque(false);
247         showPatches.setForeground(Color.WHITE);
248         showPatches.putClientProperty("JComponent.sizeVariant", "small");
249         showPatches.addActionListener(new ActionListener() {
250             public void actionPerformed(ActionEvent event) {
251                 viewer.setPatchesVisible(((JCheckBox) event.getSource()).isSelected());
252             }
253         });
254         status.add(showPatches, new GridBagConstraints(4, 1, 1, 1, 0.0f, 0.0f,
255                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
256                 new Insets(0, 12, 0, 0), 0, 0));
257 
258         JCheckBox showPadding = new JCheckBox("Show content");
259         showPadding.setOpaque(false);
260         showPadding.setForeground(Color.WHITE);
261         showPadding.putClientProperty("JComponent.sizeVariant", "small");
262         showPadding.addActionListener(new ActionListener() {
263             public void actionPerformed(ActionEvent event) {
264                 stretchesViewer.setPaddingVisible(((JCheckBox) event.getSource()).isSelected());
265             }
266         });
267         status.add(showPadding, new GridBagConstraints(5, 0, 1, 1, 0.0f, 0.0f,
268                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
269                 new Insets(0, 12, 0, 0), 0, 0));
270 
271         status.add(Box.createHorizontalGlue(), new GridBagConstraints(6, 0, 1, 1, 1.0f, 1.0f,
272                 GridBagConstraints.LINE_START, GridBagConstraints.BOTH,
273                 new Insets(0, 0, 0, 0), 0, 0));
274 
275         label = new JLabel("X: ");
276         label.setForeground(Color.WHITE);
277         label.putClientProperty("JComponent.sizeVariant", "small");
278         status.add(label, new GridBagConstraints(7, 0, 1, 1, 0.0f, 0.0f,
279                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
280                 new Insets(0, 0, 0, 0), 0, 0));
281 
282         xLabel = new JLabel("0px");
283         xLabel.setForeground(Color.WHITE);
284         xLabel.putClientProperty("JComponent.sizeVariant", "small");
285         status.add(xLabel, new GridBagConstraints(8, 0, 1, 1, 0.0f, 0.0f,
286                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
287                 new Insets(0, 0, 0, 6), 0, 0));
288 
289         label = new JLabel("Y: ");
290         label.setForeground(Color.WHITE);
291         label.putClientProperty("JComponent.sizeVariant", "small");
292         status.add(label, new GridBagConstraints(7, 1, 1, 1, 0.0f, 0.0f,
293                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
294                 new Insets(0, 0, 0, 0), 0, 0));
295 
296         yLabel = new JLabel("0px");
297         yLabel.setForeground(Color.WHITE);
298         yLabel.putClientProperty("JComponent.sizeVariant", "small");
299         status.add(yLabel, new GridBagConstraints(8, 1, 1, 1, 0.0f, 0.0f,
300                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
301                 new Insets(0, 0, 0, 6), 0, 0));
302 
303         add(status, BorderLayout.SOUTH);
304     }
305 
checkImage()306     private void checkImage() {
307         is9Patch = name.endsWith(EXTENSION_9PATCH);
308         if (!is9Patch) {
309             convertTo9Patch();
310         } else {
311             ensure9Patch();
312         }
313     }
314 
ensure9Patch()315     private void ensure9Patch() {
316         int width = image.getWidth();
317         int height = image.getHeight();
318         for (int i = 0; i < width; i++) {
319             int pixel = image.getRGB(i, 0);
320             if (pixel != 0 && pixel != 0xFF000000) {
321                 image.setRGB(i, 0, 0);
322             }
323             pixel = image.getRGB(i, height - 1);
324             if (pixel != 0 && pixel != 0xFF000000) {
325                 image.setRGB(i, height - 1, 0);
326             }
327         }
328         for (int i = 0; i < height; i++) {
329             int pixel = image.getRGB(0, i);
330             if (pixel != 0 && pixel != 0xFF000000) {
331                 image.setRGB(0, i, 0);
332             }
333             pixel = image.getRGB(width - 1, i);
334             if (pixel != 0 && pixel != 0xFF000000) {
335                 image.setRGB(width - 1, i, 0);
336             }
337         }
338     }
339 
convertTo9Patch()340     private void convertTo9Patch() {
341         BufferedImage buffer = GraphicsUtilities.createTranslucentCompatibleImage(
342                 image.getWidth() + 2, image.getHeight() + 2);
343 
344         Graphics2D g2 = buffer.createGraphics();
345         g2.drawImage(image, 1, 1, null);
346         g2.dispose();
347 
348         image = buffer;
349         name = name.substring(0, name.lastIndexOf('.')) + ".9.png";
350     }
351 
chooseSaveFile()352     File chooseSaveFile() {
353         if (is9Patch) {
354             return new File(name);
355         } else {
356             JFileChooser chooser = new JFileChooser();
357             chooser.setFileFilter(new PngFileFilter());
358             int choice = chooser.showSaveDialog(this);
359             if (choice == JFileChooser.APPROVE_OPTION) {
360                 File file = chooser.getSelectedFile();
361                 if (!file.getAbsolutePath().endsWith(EXTENSION_9PATCH)) {
362                     String path = file.getAbsolutePath();
363                     if (path.endsWith(".png")) {
364                         path = path.substring(0, path.lastIndexOf(".png")) + EXTENSION_9PATCH;
365                     } else {
366                         path = path + EXTENSION_9PATCH;
367                     }
368                     name = path;
369                     is9Patch = true;
370                     return new File(path);
371                 }
372                 is9Patch = true;
373                 return file;
374             }
375         }
376         return null;
377     }
378 
getImage()379     RenderedImage getImage() {
380         return image;
381     }
382 
383     private class StretchesViewer extends JPanel {
384         private static final int MARGIN = 24;
385 
386         private StretchView horizontal;
387         private StretchView vertical;
388         private StretchView both;
389 
390         private Dimension size;
391 
392         private float horizontalPatchesSum;
393         private float verticalPatchesSum;
394 
395         private boolean showPadding;
396 
StretchesViewer()397         StretchesViewer() {
398             setOpaque(false);
399             setLayout(new GridBagLayout());
400             setBorder(BorderFactory.createEmptyBorder(MARGIN, MARGIN, MARGIN, MARGIN));
401 
402             horizontal = new StretchView();
403             vertical = new StretchView();
404             both = new StretchView();
405 
406             setScale(DEFAULT_SCALE);
407 
408             add(vertical, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
409                     GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
410             add(horizontal, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
411                     GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
412             add(both, new GridBagConstraints(0, 2, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
413                     GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
414         }
415 
416         @Override
paintComponent(Graphics g)417         protected void paintComponent(Graphics g) {
418             Graphics2D g2 = (Graphics2D) g.create();
419             g2.setPaint(texture);
420             g2.fillRect(0, 0, getWidth(), getHeight());
421             g2.dispose();
422         }
423 
setScale(float scale)424         void setScale(float scale) {
425             int patchWidth = image.getWidth() - 2;
426             int patchHeight = image.getHeight() - 2;
427 
428             int scaledWidth = (int) (patchWidth * scale);
429             int scaledHeight = (int) (patchHeight * scale);
430 
431             horizontal.scaledWidth = scaledWidth;
432             vertical.scaledHeight = scaledHeight;
433             both.scaledWidth = scaledWidth;
434             both.scaledHeight = scaledHeight;
435 
436             size = new Dimension(scaledWidth, scaledHeight);
437 
438             computePatches();
439         }
440 
computePatches()441         void computePatches() {
442             boolean measuredWidth = false;
443             boolean endRow = true;
444 
445             int remainderHorizontal = 0;
446             int remainderVertical = 0;
447 
448             if (fixed.size() > 0) {
449                 int start = fixed.get(0).y;
450                 for (Rectangle rect : fixed) {
451                     if (rect.y > start) {
452                         endRow = true;
453                         measuredWidth = true;
454                     }
455                     if (!measuredWidth) {
456                         remainderHorizontal += rect.width;
457                     }
458                     if (endRow) {
459                         remainderVertical += rect.height;
460                         endRow = false;
461                         start = rect.y;
462                     }
463                 }
464             }
465 
466             horizontal.remainderHorizontal = horizontal.scaledWidth - remainderHorizontal;
467             vertical.remainderHorizontal = vertical.scaledWidth - remainderHorizontal;
468             both.remainderHorizontal = both.scaledWidth - remainderHorizontal;
469 
470             horizontal.remainderVertical = horizontal.scaledHeight - remainderVertical;
471             vertical.remainderVertical = vertical.scaledHeight - remainderVertical;
472             both.remainderVertical = both.scaledHeight - remainderVertical;
473 
474             horizontalPatchesSum = 0;
475             if (horizontalPatches.size() > 0) {
476                 int start = -1;
477                 for (Rectangle rect : horizontalPatches) {
478                     if (rect.x > start) {
479                         horizontalPatchesSum += rect.width;
480                         start = rect.x;
481                     }
482                 }
483             } else {
484                 int start = -1;
485                 for (Rectangle rect : patches) {
486                     if (rect.x > start) {
487                         horizontalPatchesSum += rect.width;
488                         start = rect.x;
489                     }
490                 }
491             }
492 
493             verticalPatchesSum = 0;
494             if (verticalPatches.size() > 0) {
495                 int start = -1;
496                 for (Rectangle rect : verticalPatches) {
497                     if (rect.y > start) {
498                         verticalPatchesSum += rect.height;
499                         start = rect.y;
500                     }
501                 }
502             } else {
503                 int start = -1;
504                 for (Rectangle rect : patches) {
505                     if (rect.y > start) {
506                         verticalPatchesSum += rect.height;
507                         start = rect.y;
508                     }
509                 }
510             }
511 
512             setSize(size);
513             ImageEditorPanel.this.validate();
514             repaint();
515         }
516 
setPaddingVisible(boolean visible)517         void setPaddingVisible(boolean visible) {
518             showPadding = visible;
519             repaint();
520         }
521 
522         private class StretchView extends JComponent {
523             private final Color PADDING_COLOR = new Color(0.37f, 0.37f, 1.0f, 0.5f);
524 
525             int scaledWidth;
526             int scaledHeight;
527 
528             int remainderHorizontal;
529             int remainderVertical;
530 
StretchView()531             StretchView() {
532                 scaledWidth = image.getWidth();
533                 scaledHeight = image.getHeight();
534             }
535 
536             @Override
paintComponent(Graphics g)537             protected void paintComponent(Graphics g) {
538                 int x = (getWidth() - scaledWidth) / 2;
539                 int y = (getHeight() - scaledHeight) / 2;
540 
541                 Graphics2D g2 = (Graphics2D) g.create();
542                 g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
543                         RenderingHints.VALUE_INTERPOLATION_BILINEAR);
544                 g.translate(x, y);
545 
546                 x = 0;
547                 y = 0;
548 
549                 if (patches.size() == 0) {
550                     g.drawImage(image, 0, 0, scaledWidth, scaledHeight, null);
551                     g2.dispose();
552                     return;
553                 }
554 
555                 int fixedIndex = 0;
556                 int horizontalIndex = 0;
557                 int verticalIndex = 0;
558                 int patchIndex = 0;
559 
560                 boolean hStretch;
561                 boolean vStretch;
562 
563                 float vWeightSum = 1.0f;
564                 float vRemainder = remainderVertical;
565 
566                 vStretch = verticalStartWithPatch;
567                 while (y < scaledHeight - 1) {
568                     hStretch = horizontalStartWithPatch;
569 
570                     int height = 0;
571                     float vExtra = 0.0f;
572 
573                     float hWeightSum = 1.0f;
574                     float hRemainder = remainderHorizontal;
575 
576                     while (x < scaledWidth - 1) {
577                         Rectangle r;
578                         if (!vStretch) {
579                             if (hStretch) {
580                                 r = horizontalPatches.get(horizontalIndex++);
581                                 float extra = r.width / horizontalPatchesSum;
582                                 int width = (int) (extra * hRemainder / hWeightSum);
583                                 hWeightSum -= extra;
584                                 hRemainder -= width;
585                                 g.drawImage(image, x, y, x + width, y + r.height, r.x, r.y,
586                                         r.x + r.width, r.y + r.height, null);
587                                 x += width;
588                             } else {
589                                 r = fixed.get(fixedIndex++);
590                                 g.drawImage(image, x, y, x + r.width, y + r.height, r.x, r.y,
591                                         r.x + r.width, r.y + r.height, null);
592                                 x += r.width;
593                             }
594                             height = r.height;
595                         } else {
596                             if (hStretch) {
597                                 r = patches.get(patchIndex++);
598                                 vExtra = r.height / verticalPatchesSum;
599                                 height = (int) (vExtra * vRemainder / vWeightSum);
600                                 float extra = r.width / horizontalPatchesSum;
601                                 int width = (int) (extra * hRemainder / hWeightSum);
602                                 hWeightSum -= extra;
603                                 hRemainder -= width;
604                                 g.drawImage(image, x, y, x + width, y + height, r.x, r.y,
605                                         r.x + r.width, r.y + r.height, null);
606                                 x += width;
607                             } else {
608                                 r = verticalPatches.get(verticalIndex++);
609                                 vExtra = r.height / verticalPatchesSum;
610                                 height = (int) (vExtra * vRemainder / vWeightSum);
611                                 g.drawImage(image, x, y, x + r.width, y + height, r.x, r.y,
612                                         r.x + r.width, r.y + r.height, null);
613                                 x += r.width;
614                             }
615 
616                         }
617                         hStretch = !hStretch;
618                     }
619                     x = 0;
620                     y += height;
621                     if (vStretch) {
622                         vWeightSum -= vExtra;
623                         vRemainder -= height;
624                     }
625                     vStretch = !vStretch;
626                 }
627 
628                 if (showPadding) {
629                     g.setColor(PADDING_COLOR);
630                     g.fillRect(horizontalPadding.first, verticalPadding.first,
631                             scaledWidth - horizontalPadding.first - horizontalPadding.second,
632                             scaledHeight - verticalPadding.first - verticalPadding.second);
633                 }
634 
635                 g2.dispose();
636             }
637 
638             @Override
getPreferredSize()639             public Dimension getPreferredSize() {
640                 return size;
641             }
642         }
643     }
644 
645     private class ImageViewer extends JComponent {
646         private final Color CORRUPTED_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.7f);
647         private final Color LOCK_COLOR = new Color(0.0f, 0.0f, 0.0f, 0.7f);
648         private final Color STRIPES_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.5f);
649         private final Color BACK_COLOR = new Color(0xc0c0c0);
650         private final Color HELP_COLOR = new Color(0xffffe1);
651         private final Color PATCH_COLOR = new Color(1.0f, 0.37f, 0.99f, 0.5f);
652         private final Color PATCH_ONEWAY_COLOR = new Color(0.37f, 1.0f, 0.37f, 0.5f);
653 
654         private static final float STRIPES_WIDTH = 4.0f;
655         private static final double STRIPES_SPACING = 6.0;
656         private static final int STRIPES_ANGLE = 45;
657 
658         private int zoom = DEFAULT_ZOOM;
659         private boolean showPatches;
660         private boolean showLock = true;
661 
662         private final Dimension size;
663 
664         private boolean locked;
665 
666         private int[] row;
667         private int[] column;
668 
669         private int lastPositionX;
670         private int lastPositionY;
671         private int currentButton;
672         private boolean showCursor;
673 
674         private JLabel helpLabel;
675         private boolean eraseMode;
676 
677         private JButton checkButton;
678         private List<Rectangle> corruptedPatches;
679         private boolean showBadPatches;
680 
681         private JPanel helpPanel;
682 
ImageViewer()683         ImageViewer() {
684             setLayout(new GridBagLayout());
685             helpPanel = new JPanel(new BorderLayout());
686             helpPanel.setBorder(new EmptyBorder(0, 6, 0, 6));
687             helpPanel.setBackground(HELP_COLOR);
688             helpLabel = new JLabel("Press Shift to erase pixels");
689             helpLabel.putClientProperty("JComponent.sizeVariant", "small");
690             helpPanel.add(helpLabel, BorderLayout.WEST);
691             checkButton = new JButton("Show bad patches");
692             checkButton.putClientProperty("JComponent.sizeVariant", "small");
693             checkButton.putClientProperty("JButton.buttonType", "roundRect");
694             helpPanel.add(checkButton, BorderLayout.EAST);
695 
696             add(helpPanel, new GridBagConstraints(0, 0, 1, 1,
697                     1.0f, 1.0f, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL,
698                     new Insets(0, 0, 0, 0), 0, 0));
699 
700             setOpaque(true);
701 
702             // Exact size will be set by setZoom() in AncestorListener#ancestorMoved.
703             size = new Dimension(0, 0);
704 
705             addAncestorListener(new AncestorListener() {
706                 @Override
707                 public void ancestorRemoved(AncestorEvent event) {
708                 }
709                 @Override
710                 public void ancestorMoved(AncestorEvent event) {
711                     // Set exactly size.
712                     viewer.setZoom(DEFAULT_ZOOM);
713                     viewer.removeAncestorListener(this);
714                 }
715                 @Override
716                 public void ancestorAdded(AncestorEvent event) {
717                 }
718             });
719 
720             findPatches();
721 
722             addMouseListener(new MouseAdapter() {
723                 @Override
724                 public void mousePressed(MouseEvent event) {
725                     // Store the button here instead of retrieving it again in MouseDragged
726                     // below, because on linux, calling MouseEvent.getButton() for the drag
727                     // event returns 0, which appears to be technically correct (no button
728                     // changed state).
729                     currentButton = event.isShiftDown() ? MouseEvent.BUTTON3 : event.getButton();
730                     paint(event.getX(), event.getY(), currentButton);
731                 }
732             });
733             addMouseMotionListener(new MouseMotionAdapter() {
734                 @Override
735                 public void mouseDragged(MouseEvent event) {
736                     if (!checkLockedRegion(event.getX(), event.getY())) {
737                         // use the stored button, see note above
738                         paint(event.getX(), event.getY(),  currentButton);
739                     }
740                 }
741 
742                 @Override
743                 public void mouseMoved(MouseEvent event) {
744                     checkLockedRegion(event.getX(), event.getY());
745                 }
746             });
747             Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
748                 public void eventDispatched(AWTEvent event) {
749                     enableEraseMode((KeyEvent) event);
750                 }
751             }, AWTEvent.KEY_EVENT_MASK);
752 
753             checkButton.addActionListener(new ActionListener() {
754                 public void actionPerformed(ActionEvent event) {
755                     if (!showBadPatches) {
756                         findBadPatches();
757                         checkButton.setText("Hide bad patches");
758                     } else {
759                         checkButton.setText("Show bad patches");
760                         corruptedPatches = null;
761                     }
762                     repaint();
763                     showBadPatches = !showBadPatches;
764                 }
765             });
766         }
767 
findBadPatches()768         private void findBadPatches() {
769             corruptedPatches = new ArrayList<Rectangle>();
770 
771             for (Rectangle patch : patches) {
772                 if (corruptPatch(patch)) {
773                     corruptedPatches.add(patch);
774                 }
775             }
776 
777             for (Rectangle patch : horizontalPatches) {
778                 if (corruptHorizontalPatch(patch)) {
779                     corruptedPatches.add(patch);
780                 }
781             }
782 
783             for (Rectangle patch : verticalPatches) {
784                 if (corruptVerticalPatch(patch)) {
785                     corruptedPatches.add(patch);
786                 }
787             }
788         }
789 
corruptPatch(Rectangle patch)790         private boolean corruptPatch(Rectangle patch) {
791             int[] pixels = GraphicsUtilities.getPixels(image, patch.x, patch.y,
792                     patch.width, patch.height, null);
793 
794             if (pixels.length > 0) {
795                 int reference = pixels[0];
796                 for (int pixel : pixels) {
797                     if (pixel != reference) {
798                         return true;
799                     }
800                 }
801             }
802 
803             return false;
804         }
805 
corruptHorizontalPatch(Rectangle patch)806         private boolean corruptHorizontalPatch(Rectangle patch) {
807             int[] reference = new int[patch.height];
808             int[] column = new int[patch.height];
809             reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
810                     1, patch.height, reference);
811 
812             for (int i = 1; i < patch.width; i++) {
813                 column = GraphicsUtilities.getPixels(image, patch.x + i, patch.y,
814                         1, patch.height, column);
815                 if (!Arrays.equals(reference, column)) {
816                     return true;
817                 }
818             }
819 
820             return false;
821         }
822 
corruptVerticalPatch(Rectangle patch)823         private boolean corruptVerticalPatch(Rectangle patch) {
824             int[] reference = new int[patch.width];
825             int[] row = new int[patch.width];
826             reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
827                     patch.width, 1, reference);
828 
829             for (int i = 1; i < patch.height; i++) {
830                 row = GraphicsUtilities.getPixels(image, patch.x, patch.y + i, patch.width, 1, row);
831                 if (!Arrays.equals(reference, row)) {
832                     return true;
833                 }
834             }
835 
836             return false;
837         }
838 
enableEraseMode(KeyEvent event)839         private void enableEraseMode(KeyEvent event) {
840             boolean oldEraseMode = eraseMode;
841             eraseMode = event.isShiftDown();
842             if (eraseMode != oldEraseMode) {
843                 if (eraseMode) {
844                     helpLabel.setText("Release Shift to draw pixels");
845                 } else {
846                     helpLabel.setText("Press Shift to erase pixels");
847                 }
848             }
849         }
850 
paint(int x, int y, int button)851         private void paint(int x, int y, int button) {
852             int color;
853             switch (button) {
854                 case MouseEvent.BUTTON1:
855                     color = 0xFF000000;
856                     break;
857                 case MouseEvent.BUTTON3:
858                     color = 0;
859                     break;
860                 default:
861                     return;
862             }
863 
864             int left = (getWidth() - size.width) / 2;
865             int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
866 
867             x = (x - left) / zoom;
868             y = (y - top) / zoom;
869 
870             int width = image.getWidth();
871             int height = image.getHeight();
872             if (((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) ||
873                     ((x > 0 && x < width - 1) && (y == 0 || y == height - 1))) {
874                 image.setRGB(x, y, color);
875                 findPatches();
876                 stretchesViewer.computePatches();
877                 if (showBadPatches) {
878                     findBadPatches();
879                 }
880                 repaint();
881             }
882         }
883 
checkLockedRegion(int x, int y)884         private boolean checkLockedRegion(int x, int y) {
885             int oldX = lastPositionX;
886             int oldY = lastPositionY;
887             lastPositionX = x;
888             lastPositionY = y;
889 
890             int left = (getWidth() - size.width) / 2;
891             int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
892 
893             x = (x - left) / zoom;
894             y = (y - top) / zoom;
895 
896             int width = image.getWidth();
897             int height = image.getHeight();
898 
899             xLabel.setText(Math.max(0, Math.min(x, width - 1)) + " px");
900             yLabel.setText(Math.max(0, Math.min(y, height - 1)) + " px");
901 
902             boolean previousLock = locked;
903             locked = x > 0 && x < width - 1 && y > 0 && y < height - 1;
904 
905             boolean previousCursor = showCursor;
906             showCursor = ((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) ||
907                     ((x > 0 && x < width - 1) && (y == 0 || y == height - 1));
908 
909             if (locked != previousLock) {
910                 repaint();
911             } else if (showCursor || (showCursor != previousCursor)) {
912                 Rectangle clip = new Rectangle(lastPositionX - 1 - zoom / 2,
913                         lastPositionY - 1 - zoom / 2, zoom + 2, zoom + 2);
914                 clip = clip.union(new Rectangle(oldX - 1 - zoom / 2,
915                         oldY - 1 - zoom / 2, zoom + 2, zoom + 2));
916                 repaint(clip);
917             }
918 
919             return locked;
920         }
921 
922         @Override
923         protected void paintComponent(Graphics g) {
924             int x = (getWidth() - size.width) / 2;
925             int y = helpPanel.getHeight() + (getHeight() - size.height) / 2;
926 
927             Graphics2D g2 = (Graphics2D) g.create();
928             g2.setColor(BACK_COLOR);
929             g2.fillRect(0, 0, getWidth(), getHeight());
930 
931             g2.translate(x, y);
932             g2.setPaint(texture);
933             g2.fillRect(0, 0, size.width, size.height);
934             g2.scale(zoom, zoom);
935             g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
936                     RenderingHints.VALUE_ANTIALIAS_ON);
937             g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
938                     RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
939             g2.drawImage(image, 0, 0, null);
940 
941             if (showPatches) {
942                 g2.setColor(PATCH_COLOR);
943                 for (Rectangle patch : patches) {
944                     g2.fillRect(patch.x, patch.y, patch.width, patch.height);
945                 }
946                 g2.setColor(PATCH_ONEWAY_COLOR);
947                 for (Rectangle patch : horizontalPatches) {
948                     g2.fillRect(patch.x, patch.y, patch.width, patch.height);
949                 }
950                 for (Rectangle patch : verticalPatches) {
951                     g2.fillRect(patch.x, patch.y, patch.width, patch.height);
952                 }
953             }
954 
955             if (corruptedPatches != null) {
956                 g2.setColor(CORRUPTED_COLOR);
957                 g2.setStroke(new BasicStroke(3.0f / zoom));
958                 for (Rectangle patch : corruptedPatches) {
959                     g2.draw(new RoundRectangle2D.Float(patch.x - 2.0f / zoom, patch.y - 2.0f / zoom,
960                             patch.width + 2.0f / zoom, patch.height + 2.0f / zoom,
961                             6.0f / zoom, 6.0f / zoom));
962                 }
963             }
964 
965             if (showLock && locked) {
966                 int width = image.getWidth();
967                 int height = image.getHeight();
968 
969                 g2.setColor(LOCK_COLOR);
970                 g2.fillRect(1, 1, width - 2, height - 2);
971 
972                 g2.setColor(STRIPES_COLOR);
973                 g2.translate(1, 1);
974                 paintStripes(g2, width - 2, height - 2);
975                 g2.translate(-1, -1);
976             }
977 
978             g2.dispose();
979 
980             if (showCursor) {
981                 Graphics cursor = g.create();
982                 cursor.setXORMode(Color.WHITE);
983                 cursor.setColor(Color.BLACK);
984                 cursor.drawRect(lastPositionX - zoom / 2, lastPositionY - zoom / 2, zoom, zoom);
985                 cursor.dispose();
986             }
987         }
988 
989         private void paintStripes(Graphics2D g, int width, int height) {
990             //draws pinstripes at the angle specified in this class
991             //and at the given distance apart
992             Shape oldClip = g.getClip();
993             Area area = new Area(new Rectangle(0, 0, width, height));
994             if(oldClip != null) {
995                 area = new Area(oldClip);
996             }
997             area.intersect(new Area(new Rectangle(0,0,width,height)));
998             g.setClip(area);
999 
1000             g.setStroke(new BasicStroke(STRIPES_WIDTH));
1001 
1002             double hypLength = Math.sqrt((width * width) +
1003                     (height * height));
1004 
1005             double radians = Math.toRadians(STRIPES_ANGLE);
1006             g.rotate(radians);
1007 
1008             double spacing = STRIPES_SPACING;
1009             spacing += STRIPES_WIDTH;
1010             int numLines = (int)(hypLength / spacing);
1011 
1012             for (int i=0; i<numLines; i++) {
1013                 double x = i * spacing;
1014                 Line2D line = new Line2D.Double(x, -hypLength, x, hypLength);
1015                 g.draw(line);
1016             }
1017             g.setClip(oldClip);
1018         }
1019 
1020         @Override
1021         public Dimension getPreferredSize() {
1022             return size;
1023         }
1024 
1025         void setZoom(int value) {
1026             int width = image.getWidth();
1027             int height = image.getHeight();
1028 
1029             zoom = value;
1030             if (size.height == 0 || (getHeight() - size.height) == 0) {
1031                 size.setSize(width * zoom, height * zoom + helpPanel.getHeight());
1032             } else {
1033                 size.setSize(width * zoom, height * zoom);
1034             }
1035 
1036             if (!size.equals(getSize())) {
1037                 setSize(size);
1038                 ImageEditorPanel.this.validate();
1039                 repaint();
1040             }
1041         }
1042 
1043         void setPatchesVisible(boolean visible) {
1044             showPatches = visible;
1045             findPatches();
1046             repaint();
1047         }
1048 
1049         private void findPatches() {
1050             int width = image.getWidth();
1051             int height = image.getHeight();
1052 
1053             row = GraphicsUtilities.getPixels(image, 0, 0, width, 1, row);
1054             column = GraphicsUtilities.getPixels(image, 0, 0, 1, height, column);
1055 
1056             boolean[] result = new boolean[1];
1057             Pair<List<Pair<Integer>>> left = getPatches(column, result);
1058             verticalStartWithPatch = result[0];
1059 
1060             result = new boolean[1];
1061             Pair<List<Pair<Integer>>> top = getPatches(row, result);
1062             horizontalStartWithPatch = result[0];
1063 
1064             fixed = getRectangles(left.first, top.first);
1065             patches = getRectangles(left.second, top.second);
1066 
1067             if (fixed.size() > 0) {
1068                 horizontalPatches = getRectangles(left.first, top.second);
1069                 verticalPatches = getRectangles(left.second, top.first);
1070             } else {
1071                 if (top.first.size() > 0) {
1072                     horizontalPatches = new ArrayList<Rectangle>(0);
1073                     verticalPatches = getVerticalRectangles(top.first);
1074                 } else if (left.first.size() > 0) {
1075                     horizontalPatches = getHorizontalRectangles(left.first);
1076                     verticalPatches = new ArrayList<Rectangle>(0);
1077                 } else {
1078                     horizontalPatches = verticalPatches = new ArrayList<Rectangle>(0);
1079                 }
1080             }
1081 
1082             row = GraphicsUtilities.getPixels(image, 0, height - 1, width, 1, row);
1083             column = GraphicsUtilities.getPixels(image, width - 1, 0, 1, height, column);
1084 
1085             top = getPatches(row, result);
1086             horizontalPadding = getPadding(top.first);
1087 
1088             left = getPatches(column, result);
1089             verticalPadding = getPadding(left.first);
1090         }
1091 
getVerticalRectangles(List<Pair<Integer>> topPairs)1092         private List<Rectangle> getVerticalRectangles(List<Pair<Integer>> topPairs) {
1093             List<Rectangle> rectangles = new ArrayList<Rectangle>();
1094             for (Pair<Integer> top : topPairs) {
1095                 int x = top.first;
1096                 int width = top.second - top.first;
1097 
1098                 rectangles.add(new Rectangle(x, 1, width, image.getHeight() - 2));
1099             }
1100             return rectangles;
1101         }
1102 
getHorizontalRectangles(List<Pair<Integer>> leftPairs)1103         private List<Rectangle> getHorizontalRectangles(List<Pair<Integer>> leftPairs) {
1104             List<Rectangle> rectangles = new ArrayList<Rectangle>();
1105             for (Pair<Integer> left : leftPairs) {
1106                 int y = left.first;
1107                 int height = left.second - left.first;
1108 
1109                 rectangles.add(new Rectangle(1, y, image.getWidth() - 2, height));
1110             }
1111             return rectangles;
1112         }
1113 
getPadding(List<Pair<Integer>> pairs)1114         private Pair<Integer> getPadding(List<Pair<Integer>> pairs) {
1115             if (pairs.size() == 0) {
1116                 return new Pair<Integer>(0, 0);
1117             } else if (pairs.size() == 1) {
1118                 if (pairs.get(0).first == 1) {
1119                     return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first, 0);
1120                 } else {
1121                     return new Pair<Integer>(0, pairs.get(0).second - pairs.get(0).first);
1122                 }
1123             } else {
1124                 int index = pairs.size() - 1;
1125                 return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first,
1126                         pairs.get(index).second - pairs.get(index).first);
1127             }
1128         }
1129 
getRectangles(List<Pair<Integer>> leftPairs, List<Pair<Integer>> topPairs)1130         private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs,
1131                 List<Pair<Integer>> topPairs) {
1132             List<Rectangle> rectangles = new ArrayList<Rectangle>();
1133             for (Pair<Integer> left : leftPairs) {
1134                 int y = left.first;
1135                 int height = left.second - left.first;
1136                 for (Pair<Integer> top : topPairs) {
1137                     int x = top.first;
1138                     int width = top.second - top.first;
1139 
1140                     rectangles.add(new Rectangle(x, y, width, height));
1141                 }
1142             }
1143             return rectangles;
1144         }
1145 
getPatches(int[] pixels, boolean[] startWithPatch)1146         private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) {
1147             int lastIndex = 1;
1148             int lastPixel = pixels[1];
1149             boolean first = true;
1150 
1151             List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>();
1152             List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>();
1153 
1154             for (int i = 1; i < pixels.length - 1; i++) {
1155                 int pixel = pixels[i];
1156                 if (pixel != lastPixel) {
1157                     if (lastPixel == 0xFF000000) {
1158                         if (first) startWithPatch[0] = true;
1159                         patches.add(new Pair<Integer>(lastIndex, i));
1160                     } else {
1161                         fixed.add(new Pair<Integer>(lastIndex, i));
1162                     }
1163                     first = false;
1164 
1165                     lastIndex = i;
1166                     lastPixel = pixel;
1167                 }
1168             }
1169             if (lastPixel == 0xFF000000) {
1170                 if (first) startWithPatch[0] = true;
1171                 patches.add(new Pair<Integer>(lastIndex, pixels.length - 1));
1172             } else {
1173                 fixed.add(new Pair<Integer>(lastIndex, pixels.length - 1));
1174             }
1175 
1176             if (patches.size() == 0) {
1177                 patches.add(new Pair<Integer>(1, pixels.length - 1));
1178                 startWithPatch[0] = true;
1179                 fixed.clear();
1180             }
1181 
1182             return new Pair<List<Pair<Integer>>>(fixed, patches);
1183         }
1184 
setLockVisible(boolean visible)1185         void setLockVisible(boolean visible) {
1186             showLock = visible;
1187             repaint();
1188         }
1189     }
1190 
1191     static class Pair<E> {
1192         E first;
1193         E second;
1194 
Pair(E first, E second)1195         Pair(E first, E second) {
1196             this.first = first;
1197             this.second = second;
1198         }
1199 
1200         @Override
toString()1201         public String toString() {
1202             return "Pair[" + first + ", " + second + "]";
1203         }
1204     }
1205 }
1206