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