├── .gitignore
├── README.md
├── pom.xml
└── src
├── main
├── java
│ └── io
│ │ └── github
│ │ └── elime1
│ │ └── piceditor
│ │ ├── application
│ │ ├── App.java
│ │ ├── AppInfo.java
│ │ ├── AppPreferences.java
│ │ └── spring
│ │ │ ├── SpringConfig.java
│ │ │ └── SpringFxmlLoader.java
│ │ ├── controllers
│ │ └── MainViewController.java
│ │ ├── models
│ │ ├── ImageDisplayInfo.java
│ │ ├── Pic.java
│ │ ├── PicColor.java
│ │ ├── PicDisplayInfo.java
│ │ ├── PicImage.java
│ │ └── Sprite.java
│ │ ├── service
│ │ ├── ImageService.java
│ │ ├── PicService.java
│ │ ├── VersionService.java
│ │ └── exceptions
│ │ │ ├── UnsupportedPicFormatException.java
│ │ │ └── WrongImageDimensionsException.java
│ │ ├── ui
│ │ ├── FileChooserDialog.java
│ │ └── FileType.java
│ │ └── utils
│ │ ├── ImageIndex.java
│ │ ├── PicIO.java
│ │ ├── PicImageConverter.java
│ │ ├── ReadableDataBuffer.java
│ │ └── WritableDataBuffer.java
└── resources
│ ├── css
│ └── style.css
│ ├── img
│ └── icon.png
│ ├── log4j2.xml
│ ├── versions
│ └── versions.xml
│ └── view
│ └── main.fxml
└── test
└── java
└── io
└── github
└── elime1
└── piceditor
└── PicServiceTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /PicEditor.iml
3 | /target
4 | *.pic
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Elime's Pic Editor
2 | With this application you can replace the images stored in the Tibia.pic file. This will change the appearance of the Tibia client.
3 |
4 | ### Version support
5 | You can edit any pic file distributed with Tibia client 7.0 up to 10.99.
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | 4.0.0
7 |
8 | io.github.elime1
9 | piceditor
10 | 1.2.0
11 | PicEditor
12 |
13 | jar
14 |
15 |
16 | UTF-8
17 |
18 | PicEditor
19 | Editor for tibia pic files
20 |
21 | 2.8
22 |
23 | 5.0.2.RELEASE
24 | 1.16.16
25 | 1.2.5
26 | 4.12
27 | 2.5
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | org.springframework
36 | spring-context
37 | ${spring.version}
38 |
39 |
40 |
41 |
42 | org.projectlombok
43 | lombok
44 | ${lombok.version}
45 |
46 |
47 |
48 |
49 | xom
50 | xom
51 | ${xom.version}
52 |
53 |
54 |
55 |
56 | junit
57 | junit
58 | ${junit.version}
59 | test
60 |
61 |
62 | org.springframework
63 | spring-test
64 | ${spring.version}
65 | test
66 |
67 |
68 | org.mockito
69 | mockito-all
70 | 1.10.19
71 | test
72 |
73 |
74 |
75 |
76 | org.apache.logging.log4j
77 | log4j-api
78 | ${log4j2.version}
79 |
80 |
81 | org.apache.logging.log4j
82 | log4j-core
83 | ${log4j2.version}
84 |
85 |
86 | org.apache.logging.log4j
87 | log4j-jcl
88 | ${log4j2.version}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | org.apache.maven.plugins
97 | maven-compiler-plugin
98 | 3.3
99 |
100 | 1.8
101 | 1.8
102 |
103 |
104 |
105 | maven-assembly-plugin
106 |
107 |
108 |
109 | io.github.elime1.piceditor.application.App
110 |
111 |
112 |
113 | jar-with-dependencies
114 |
115 |
116 |
117 |
118 | assembly
119 | package
120 |
121 | single
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/application/App.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.application;
2 |
3 | import io.github.elime1.piceditor.application.spring.SpringConfig;
4 | import io.github.elime1.piceditor.application.spring.SpringFxmlLoader;
5 | import io.github.elime1.piceditor.controllers.MainViewController;
6 | import javafx.application.Application;
7 | import javafx.scene.Parent;
8 | import javafx.scene.Scene;
9 | import javafx.scene.control.Alert;
10 | import javafx.scene.image.Image;
11 | import javafx.stage.Stage;
12 | import org.apache.logging.log4j.LogManager;
13 | import org.apache.logging.log4j.Logger;
14 | import org.springframework.context.ApplicationContext;
15 | import org.springframework.context.annotation.AnnotationConfigApplicationContext;
16 |
17 | import java.io.IOException;
18 | import java.util.List;
19 |
20 | public class App extends Application {
21 |
22 | private static Logger log = LogManager.getLogger();
23 | private Stage window;
24 |
25 | public static void main(String[] args) throws IOException {
26 | launch(args);
27 | }
28 |
29 | @Override
30 | public void start(Stage window) throws Exception {
31 |
32 | ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
33 | AppInfo appInfo = context.getBean(AppInfo.class);
34 |
35 | Thread.currentThread().setName("Main thread");
36 |
37 | log.info("Starting " + appInfo.getAppName());
38 | log.info("Version " + appInfo.getAppVersion());
39 |
40 | this.window = window;
41 | this.window.setTitle(appInfo.getAppName() + " " + appInfo.getAppVersion());
42 | this.window.getIcons().add(new Image(App.class.getResourceAsStream("/img/icon.png")));
43 |
44 | //onCloseRequest is not triggered when calling window.close() so we use onHiding instead
45 | window.setOnHiding(event -> {
46 | log.info("Exiting PicEditor");
47 | });
48 |
49 | log.debug("Setup uncaught exception handler");
50 | setupExceptionHandling();
51 |
52 | log.debug("Creating scene...");
53 | window.setScene(createScene(context));
54 | log.debug("Scene created");
55 |
56 | window.centerOnScreen();
57 |
58 | log.debug("Showing application window");
59 | window.show();
60 |
61 | //Check for command-line argument
62 | List parameters = getParameters().getUnnamed();
63 | if (!parameters.isEmpty()) {
64 | context.getBean(MainViewController.class).openPic(parameters.get(0));
65 | }
66 | }
67 |
68 | private void setupExceptionHandling() {
69 | Thread.currentThread().setUncaughtExceptionHandler((thread, throwable) -> showErrorAlert(throwable));
70 | }
71 |
72 | private Scene createScene(ApplicationContext context) throws IOException {
73 | String css = getClass().getResource("/css/style.css").toExternalForm();
74 | SpringFxmlLoader loader = new SpringFxmlLoader(context);
75 | Parent parent = (Parent) loader.load("/view/main.fxml");
76 | Scene scene = new Scene(parent, 790, 480);
77 | scene.getStylesheets().add(css);
78 | return scene;
79 | }
80 |
81 | private void showErrorAlert(Throwable throwable) {
82 | String message = throwable.getMessage();
83 | if (message == null) {
84 | message = throwable.toString();
85 | }
86 | log.error(message, throwable);
87 | showErrorAlert(message);
88 | }
89 |
90 | private void showErrorAlert(String message) {
91 | Alert alert = new Alert(Alert.AlertType.WARNING);
92 | alert.setTitle("Error");
93 | alert.setHeaderText(null);
94 | alert.setContentText(message);
95 | alert.initOwner(this.window);
96 | alert.show();
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/application/AppInfo.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.application;
2 |
3 | import lombok.Getter;
4 | import org.springframework.stereotype.Component;
5 |
6 | @Getter
7 | @Component
8 | public class AppInfo {
9 | private final String appName = "Elime's Pic Editor";
10 | private final String appVersion = "1.2.0";
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/application/AppPreferences.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.application;
2 |
3 | import org.springframework.stereotype.Component;
4 | import java.util.prefs.Preferences;
5 |
6 | @Component
7 | public class AppPreferences {
8 |
9 | private static final String ROOT_NODE = "elime.piceditor";
10 | private static final String USER_HOME_PROPRTY = "user.home";
11 | private static final String PIC_PATH_PROPRTY = "picPath";
12 | private static final String IMAGE_PATH_PROPRTY = "imagePath";
13 |
14 | private Preferences preferences;
15 |
16 | public AppPreferences() {
17 | preferences = Preferences.userRoot().node(ROOT_NODE);
18 | }
19 |
20 | public void setPicPath(String path) {
21 | preferences.put(PIC_PATH_PROPRTY, path);
22 | }
23 |
24 | public String getPicPath() {
25 | return preferences.get(PIC_PATH_PROPRTY, System.getProperty(USER_HOME_PROPRTY));
26 | }
27 |
28 | public void setImagePath(String path) {
29 | preferences.put(IMAGE_PATH_PROPRTY, path);
30 | }
31 |
32 | public String getImagePath() {
33 | return preferences.get(IMAGE_PATH_PROPRTY, System.getProperty(USER_HOME_PROPRTY));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/application/spring/SpringConfig.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.application.spring;
2 |
3 | import org.springframework.context.annotation.ComponentScan;
4 | import org.springframework.context.annotation.Configuration;
5 |
6 | @Configuration
7 | @ComponentScan(basePackages = "io.github.elime1.piceditor")
8 | public class SpringConfig {}
9 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/application/spring/SpringFxmlLoader.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.application.spring;
2 |
3 | import javafx.fxml.FXMLLoader;
4 | import org.springframework.beans.BeansException;
5 | import org.springframework.context.ApplicationContext;
6 |
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 | import java.net.URL;
10 |
11 | public class SpringFxmlLoader {
12 |
13 | private ApplicationContext context;
14 |
15 | public SpringFxmlLoader(ApplicationContext appContext) {
16 | this.context = appContext;
17 | }
18 |
19 | public Object load(final String resource) throws IOException {
20 | try (InputStream fxmlStream = getClass().getResourceAsStream(resource)) {
21 | FXMLLoader loader = new FXMLLoader();
22 | URL location = getClass().getResource(resource);
23 | loader.setLocation(location);
24 | loader.setControllerFactory(context::getBean);
25 | return loader.load(fxmlStream);
26 | } catch (BeansException e) {
27 | throw new RuntimeException(e);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/controllers/MainViewController.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.controllers;
2 |
3 | import io.github.elime1.piceditor.models.ImageDisplayInfo;
4 | import io.github.elime1.piceditor.models.PicDisplayInfo;
5 | import io.github.elime1.piceditor.service.exceptions.WrongImageDimensionsException;
6 | import io.github.elime1.piceditor.ui.FileChooserDialog;
7 | import io.github.elime1.piceditor.service.ImageService;
8 | import io.github.elime1.piceditor.service.PicService;
9 | import io.github.elime1.piceditor.service.VersionService;
10 | import javafx.fxml.FXML;
11 | import javafx.fxml.Initializable;
12 | import javafx.geometry.Pos;
13 | import javafx.scene.control.*;
14 | import javafx.scene.image.Image;
15 | import javafx.scene.image.ImageView;
16 | import javafx.scene.input.ClipboardContent;
17 | import javafx.scene.input.Dragboard;
18 | import javafx.scene.input.KeyCode;
19 | import javafx.scene.input.TransferMode;
20 | import javafx.scene.layout.AnchorPane;
21 | import javafx.stage.Window;
22 | import lombok.extern.log4j.Log4j2;
23 | import org.springframework.beans.factory.annotation.Autowired;
24 | import org.springframework.stereotype.Component;
25 |
26 | import java.io.File;
27 | import java.net.URL;
28 | import java.util.Collections;
29 | import java.util.ResourceBundle;
30 |
31 | import static java.util.Objects.nonNull;
32 | import static javafx.scene.control.Alert.AlertType.WARNING;
33 |
34 | @Component
35 | @Log4j2
36 | public class MainViewController implements Initializable {
37 |
38 | @FXML private AnchorPane mainAnchorPain;
39 |
40 | //Top
41 | @FXML private ProgressBar progressBar;
42 | @FXML private Label imageCountLabel;
43 | @FXML private Button previousImageButton;
44 | @FXML private Button nextImageButton;
45 |
46 | //Left
47 | @FXML private Label versionLabel;
48 | @FXML private Label signatureLabel;
49 | @FXML private Label numberOfImagesLabel;
50 | @FXML private Label imageDimensionsLabel;
51 | @FXML private Label imageBackgroundDisplayLabel;
52 | @FXML private Button replaceCurrentImageButton;
53 | @FXML private Button saveCurrentImageButton;
54 | @FXML private Button openPicButton;
55 | @FXML private Button savePicButton;
56 |
57 | //Center
58 | @FXML private ImageView imageView;
59 |
60 | private FileChooserDialog fileChooserDialog;
61 | private ImageService imageService;
62 | private PicService picService;
63 |
64 | private boolean dragFromImageView = false;
65 |
66 | @Autowired
67 | MainViewController(PicService picService, VersionService versionService, ImageService imageService) {
68 | this.picService = picService;
69 | this.imageService = imageService;
70 | }
71 |
72 | @Autowired
73 | public void setFileChooserDialog(FileChooserDialog fileChooserDialog) {
74 | this.fileChooserDialog = fileChooserDialog;
75 | }
76 |
77 | ////////////////////////////////////////////////////////////////////////
78 | //Initialize
79 |
80 | @Override
81 | public void initialize(URL location, ResourceBundle resources) {
82 | progressBar.setProgress(0.0);
83 | imageCountLabel.setAlignment(Pos.CENTER);
84 | initButtonEvents();
85 | initDragEvents();
86 | initKeyListener();
87 | }
88 |
89 | private void initButtonEvents() {
90 | replaceCurrentImageButton.setOnAction(event -> replaceImage());
91 | saveCurrentImageButton.setOnAction(event -> saveImage());
92 | openPicButton.setOnAction(event -> openPic());
93 | savePicButton.setOnAction(event -> savePic());
94 | nextImageButton.setOnAction(event -> nextImage());
95 | previousImageButton.setOnAction(event -> previousImage());
96 | }
97 |
98 | private void initDragEvents() {
99 |
100 | imageView.setOnDragDetected(event -> {
101 | if (picService.isPicLoaded()) {
102 | dragFromImageView = true;
103 | String tmpDir = System.getProperty("user.home") + "/image.png";
104 | Dragboard db = imageView.startDragAndDrop(TransferMode.ANY);
105 | ClipboardContent clipboardContent = new ClipboardContent();
106 | File imageFile = new File(tmpDir);
107 | clipboardContent.putFiles(Collections.singletonList(imageFile));
108 | db.setContent(clipboardContent);
109 | imageService.saveImage(imageFile, imageView.getImage());
110 | event.consume();
111 | }
112 | });
113 |
114 | imageView.setOnDragOver(event -> {
115 | if (!dragFromImageView) {
116 | Dragboard db = event.getDragboard();
117 | if (db.hasFiles()) {
118 | File file = db.getFiles().get(0);
119 | if (file.getName().toLowerCase().endsWith(".png")) {
120 | event.acceptTransferModes(TransferMode.COPY);
121 | event.consume();
122 | }
123 | }
124 | }
125 | });
126 |
127 | imageView.setOnDragDropped(event -> {
128 | log.debug("Mouse drag dropped on image view");
129 | if (picService.isPicLoaded()) {
130 | Dragboard db = event.getDragboard();
131 | if (db.hasFiles()) {
132 | replaceCurrentImage(db.getFiles().get(0));
133 | }
134 | }
135 | event.setDropCompleted(true);
136 | event.consume();
137 | });
138 |
139 | imageView.setOnDragDone(event -> {
140 | log.debug("Mouse drag done");
141 | dragFromImageView = false;
142 | });
143 |
144 | }
145 |
146 | private void initKeyListener() {
147 | mainAnchorPain.setOnKeyPressed(event -> {
148 | KeyCode keyCode = event.getCode();
149 | if (keyCode.equals(KeyCode.RIGHT)) {nextImage();}
150 | else if (keyCode.equals(KeyCode.LEFT)) {previousImage();}
151 | else if (event.isControlDown()) {
152 | if (event.isAltDown()) {
153 | if (keyCode.equals(KeyCode.O)) {replaceImage();}
154 | else if (keyCode.equals(KeyCode.S)) {saveImage();}
155 | } else {
156 | if (keyCode.equals(KeyCode.O)) {openPic();}
157 | else if (keyCode.equals(KeyCode.S)) {savePic();}
158 | else if (keyCode.equals(KeyCode.R)) {replaceImage();}
159 | }
160 | }
161 | });
162 | }
163 |
164 | ////////////////////////////////////////////////////////////////////////
165 | //Public
166 |
167 | public void openPic(String fileName) {
168 | log.debug("Opening file: " + fileName);
169 | String extension = fileName.substring(fileName.length() - 4);
170 | extension = extension.toLowerCase();
171 | if (extension.equals(".pic")) {
172 | openPic(new File(fileName));
173 | } else {
174 | log.debug("The file is not a pic file: " + fileName);
175 | showAlert("Failed to open pic", "The file '" + fileName + "' does not seem to be a pic file.");
176 | }
177 | }
178 |
179 | ////////////////////////////////////////////////////////////////////////
180 | //Private
181 |
182 | private void openPic() {
183 | openPic(fileChooserDialog.showOpenPicDialog(getWindow()));
184 | }
185 |
186 | private void openPic(File file) {
187 | if (nonNull(file)) {
188 | if (file.exists()) {
189 | try {
190 | loadPic(file);
191 | } catch (Exception e) {
192 | log.error("Failed to open pic", e);
193 | showAlert("Failed to open pic",
194 | "The file '" + file.getPath() + "' could not be opened: " + e.toString());
195 | }
196 | } else {
197 | log.warn("The file '" + file.getPath() + " does not exist!");
198 | }
199 | }
200 | }
201 |
202 | private void replaceCurrentImage(File file) {
203 | try {
204 | picService.replacePicImageSprites(file);
205 | } catch (WrongImageDimensionsException e) {
206 | showAlert("Wrong dimensions!", e.getMessage());
207 | return;
208 | }
209 | int imageNumber = picService.getCurrentImageNumber();
210 | log.info("Replacing image " + imageNumber + " with: " + file.getPath());
211 | imageView.setImage(picService.currentImage());
212 | }
213 |
214 | private void nextImage() {
215 | if (picService.isPicLoaded()) {
216 | picService.nextImage();
217 | displayImage();
218 | }
219 | }
220 |
221 | private void previousImage() {
222 | if (picService.isPicLoaded()) {
223 | picService.previousImage();
224 | displayImage();
225 | }
226 | }
227 |
228 | private void savePic() {
229 | if (picService.isPicLoaded()) {
230 | File file = fileChooserDialog.showSavePicDialog(getWindow());
231 | if (nonNull(file)) {
232 | log.debug("Saving pic: " + file.getPath());
233 | picService.savePic(file);
234 | }
235 | }
236 | }
237 |
238 | private void replaceImage() {
239 | if (picService.isPicLoaded()) {
240 | File file = fileChooserDialog.showReplaceImageDialog(getWindow());
241 | if (nonNull(file)) {
242 | replaceCurrentImage(file);
243 | }
244 | }
245 | }
246 |
247 | private void saveImage() {
248 | Image image = imageView.getImage();
249 | if (nonNull(image)) {
250 | File file = fileChooserDialog.showSaveImageDialog(getWindow());
251 | if (nonNull(file)) {
252 | log.debug("Saving image: " + file.getPath());
253 | imageService.saveImage(file, image);
254 | }
255 | }
256 | }
257 |
258 | private void displayImage() {
259 | ImageDisplayInfo info = picService.getImageDisplayInfo();
260 | Tooltip tooltip = new Tooltip(info.getBgRgb() + " " + info.getBgHex());
261 | imageCountLabel.setText(info.getImageCount());
262 | imageDimensionsLabel.setText(info.getImageDimensions());
263 | imageBackgroundDisplayLabel.setStyle("-fx-background-color:" + info.getBgRgb() + ";");
264 | imageBackgroundDisplayLabel.setTooltip(tooltip);
265 | imageView.setImage(picService.currentImage());
266 | }
267 |
268 | private void showNavigationArrows(boolean visible) {
269 | previousImageButton.setVisible(visible);
270 | nextImageButton.setVisible(visible);
271 | }
272 |
273 | private void loadPic(File picFile) {
274 | picService.loadPic(picFile);
275 | updatePicInfo();
276 | showNavigationArrows(true);
277 | displayImage();
278 | }
279 |
280 | private void updatePicInfo() {
281 | PicDisplayInfo info = picService.getPicDisplayInfo();
282 | versionLabel.setText(info.getVersion());
283 | signatureLabel.setText(info.getSignature());
284 | numberOfImagesLabel.setText(info.getNumberOfImages());
285 | }
286 |
287 | private Window getWindow() {
288 | if (nonNull(mainAnchorPain.getScene())) {
289 | return mainAnchorPain.getScene().getWindow();
290 | }
291 | return null;
292 | }
293 |
294 | private void showAlert(String title, String message) {
295 | Alert alert = new Alert(WARNING);
296 | alert.setTitle(title);
297 | alert.setHeaderText(null);
298 | alert.setContentText(message);
299 | alert.initOwner(getWindow());
300 | alert.show();
301 | }
302 |
303 | }
304 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/models/ImageDisplayInfo.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.models;
2 |
3 | import lombok.Builder;
4 | import lombok.Getter;
5 |
6 | @Getter
7 | @Builder
8 | public class ImageDisplayInfo {
9 | private String imageCount;
10 | private String imageDimensions;
11 | private String bgHex;
12 | private String bgRgb;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/models/Pic.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.models;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class Pic {
7 | private PicImage[] picImages;
8 | private int signature;
9 | private int numberOfImages;
10 | private int numberOfBytes;
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/models/PicColor.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.models;
2 |
3 | import lombok.Getter;
4 |
5 | @Getter
6 | public class PicColor {
7 |
8 | private byte r;
9 | private byte g;
10 | private byte b;
11 |
12 | public PicColor(byte r, byte g, byte b) {
13 | this.r = r;
14 | this.g = g;
15 | this.b = b;
16 | }
17 |
18 | public String toRGBString() {
19 | return new StringBuilder("rgb(")
20 | .append(r & 0xFF).append(",")
21 | .append(g & 0xFF).append(",")
22 | .append(b & 0xFF).append(")")
23 | .toString();
24 | }
25 |
26 | public String toHexString() {
27 | return String.format("#%06X", (r & 0xFF) << 16 | (g & 0xFF) << 8 | b & 0xFF);
28 | }
29 |
30 | @Override
31 | public String toString() {
32 | return toRGBString();
33 | }
34 |
35 | @Override
36 | public boolean equals(Object o) {
37 | if (this == o) return true;
38 | if (o == null || getClass() != o.getClass()) return false;
39 | PicColor c = (PicColor) o;
40 | return r == c.r && g == c.g && b == c.b;
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/models/PicDisplayInfo.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.models;
2 |
3 | import lombok.Builder;
4 | import lombok.Getter;
5 |
6 | @Builder
7 | @Getter
8 | public class PicDisplayInfo {
9 | private String version;
10 | private String signature;
11 | private String numberOfImages;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/models/PicImage.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.models;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.Data;
5 | import lombok.Setter;
6 |
7 | @Data
8 | public class PicImage {
9 | @Setter(AccessLevel.NONE) private Pic pic;
10 | private Sprite[] sprites;
11 | private int width;
12 | private int height;
13 | private PicColor bgColor;
14 |
15 | public PicImage(Pic pic) {
16 | this.pic = pic;
17 | }
18 |
19 | public Sprite[] getSprites() {
20 | return sprites;
21 | }
22 |
23 | public void setSprites(Sprite[] sprites) {
24 | if (this.sprites != null) {
25 | //We need to make sure the pic byte size is correct when we replace sprites
26 | pic.setNumberOfBytes(pic.getNumberOfBytes() - getSpriteDataByteSize());
27 | this.sprites = sprites;
28 | pic.setNumberOfBytes(pic.getNumberOfBytes() + getSpriteDataByteSize());
29 | } else {
30 | this.sprites = sprites;
31 | }
32 | }
33 |
34 | public int getSpriteDataByteSize() {
35 | int nBytes = 0;
36 | for (Sprite sprite : sprites) {
37 | nBytes += sprite.getPixelData().length + Integer.BYTES + Short.BYTES;
38 | }
39 | return nBytes;
40 | }
41 |
42 | public int getPixelWidth() {
43 | return width * Sprite.DEFAULT_W_H;
44 | }
45 |
46 | public int getPixelHeight() {
47 | return height * Sprite.DEFAULT_W_H;
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/models/Sprite.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.models;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class Sprite {
7 |
8 | public static final int DEFAULT_W_H = 32;
9 |
10 | private byte[] pixelData;
11 |
12 | public Sprite(byte[] pixelData) {
13 | this.pixelData = pixelData;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/service/ImageService.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.service;
2 |
3 | import javafx.embed.swing.SwingFXUtils;
4 | import javafx.scene.image.*;
5 | import lombok.extern.log4j.Log4j2;
6 | import org.springframework.stereotype.Service;
7 |
8 | import javax.imageio.ImageIO;
9 | import java.awt.image.BufferedImage;
10 | import java.io.File;
11 | import java.io.FileInputStream;
12 | import java.io.FileNotFoundException;
13 | import java.io.IOException;
14 |
15 | @Log4j2
16 | @Service
17 | public class ImageService {
18 |
19 | public void saveImage(File file, Image image) {
20 | if (image == null) return;
21 | log.info("Saving image to file: " + file.getPath());
22 | try {
23 | String name = file.getName();
24 | String extension = name.substring(name.lastIndexOf('.') + 1, name.length()).toLowerCase();
25 | if (extension.equals("png")) { //With alpha
26 | ImageIO.write(SwingFXUtils.fromFXImage(image, null), extension, file);
27 | } else { //Without alpha (jpg, bmp)
28 | ImageIO.write(convertToBufferedImageWithoutAlpha(image), extension, file);
29 | }
30 | } catch (IOException e) {
31 | log.warn("Failed to write image to file");
32 | throw new RuntimeException(e);
33 | } catch (IllegalArgumentException e) {
34 | log.warn("Could not save image - null argument");
35 | throw new RuntimeException(e);
36 | }
37 | }
38 |
39 | public Image openImage(File file) {
40 | log.info("Opening image from file: " + file.getPath());
41 |
42 | FileInputStream fileInputStream = null;
43 | Image image = null;
44 |
45 | try {
46 | fileInputStream = new FileInputStream(file);
47 | image = new Image(fileInputStream);
48 | } catch (FileNotFoundException e) {
49 | log.warn("Could not open image file: " + file.getPath());
50 | throw new RuntimeException(e);
51 | } finally {
52 | try {
53 | if (fileInputStream != null) fileInputStream.close();
54 | } catch (IOException e) {
55 | log.warn("Failed to close FileInputStream", e);
56 | }
57 | }
58 |
59 | return image;
60 | }
61 |
62 | private BufferedImage convertToBufferedImageWithoutAlpha(Image image) {
63 | BufferedImage bufferedImage = new BufferedImage((int)image.getWidth(), (int)image.getHeight(), BufferedImage.TYPE_INT_RGB);
64 | PixelReader pixelReader = image.getPixelReader();
65 | for (int y = 0; y < bufferedImage.getHeight(); y++) {
66 | for (int x = 0; x < bufferedImage.getWidth(); x++) {
67 | bufferedImage.setRGB(x, y, pixelReader.getArgb(x, y));
68 | }
69 | }
70 | return bufferedImage;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/service/PicService.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.service;
2 |
3 | import io.github.elime1.piceditor.models.ImageDisplayInfo;
4 | import io.github.elime1.piceditor.models.Pic;
5 | import io.github.elime1.piceditor.models.PicDisplayInfo;
6 | import io.github.elime1.piceditor.models.PicImage;
7 | import io.github.elime1.piceditor.service.exceptions.UnsupportedPicFormatException;
8 | import io.github.elime1.piceditor.service.exceptions.WrongImageDimensionsException;
9 | import io.github.elime1.piceditor.utils.ImageIndex;
10 | import io.github.elime1.piceditor.utils.PicIO;
11 | import io.github.elime1.piceditor.utils.PicImageConverter;
12 | import javafx.scene.image.Image;
13 | import lombok.extern.log4j.Log4j2;
14 | import org.springframework.beans.factory.annotation.Autowired;
15 | import org.springframework.stereotype.Service;
16 |
17 | import java.io.File;
18 |
19 | import static java.util.Objects.nonNull;
20 |
21 | @Log4j2
22 | @Service
23 | public class PicService {
24 |
25 | private Pic pic;
26 | private VersionService versionService;
27 | private ImageService imageService;
28 | private PicIO picIO;
29 | private PicImageConverter picImageConverter;
30 |
31 | private ImageIndex imageIndex;
32 |
33 |
34 | @Autowired
35 | public PicService(VersionService versionService,
36 | ImageService imageService,
37 | PicIO picIO,
38 | PicImageConverter picImageConverter) {
39 | this.versionService = versionService;
40 | this.imageService = imageService;
41 | this.picIO = picIO;
42 | this.picImageConverter = picImageConverter;
43 | }
44 |
45 | public void loadPic(File picFile) {
46 | try {
47 | pic = readPic(picFile);
48 | imageIndex = new ImageIndex(pic.getNumberOfImages(), 0);
49 | } catch (UnsupportedPicFormatException e) {
50 | log.warn(e.getMessage());
51 | throw new RuntimeException("Unsupported pic", e);
52 | }
53 | }
54 |
55 | public void savePic(File picFile) {
56 | writePic(picFile, pic);
57 | }
58 |
59 | public Image currentImage() {
60 | return getImageFromPicImage(currentPicImage());
61 | }
62 |
63 | public boolean isPicLoaded() {
64 | return nonNull(pic);
65 | }
66 |
67 | public int getCurrentImageNumber() {
68 | return imageIndex.currentImageNumber();
69 | }
70 |
71 | public void nextImage() {
72 | imageIndex.nexImage();
73 | }
74 |
75 | public void previousImage() {
76 | imageIndex.previousImage();
77 | }
78 |
79 | private PicImage currentPicImage() {
80 | return pic.getPicImages()[imageIndex.currentIndex()];
81 | }
82 |
83 | public PicDisplayInfo getPicDisplayInfo() {
84 | if (pic.getNumberOfImages() > 0) {
85 | String signatureHex = Integer.toHexString(pic.getSignature());
86 | String version = versionService.getTibiaVersion(signatureHex);
87 | int numberOfImages = pic.getNumberOfImages();
88 |
89 | return PicDisplayInfo.builder()
90 | .version("Version: " + (version == null? "Unknown" : version))
91 | .signature("Signature: " + signatureHex)
92 | .numberOfImages("Images: " + numberOfImages)
93 | .build();
94 | }
95 | return null;
96 | }
97 |
98 | public ImageDisplayInfo getImageDisplayInfo() {
99 | PicImage picImage = currentPicImage();
100 | return ImageDisplayInfo.builder()
101 | .imageCount(imageIndex.currentImageNumber() + "/" + pic.getNumberOfImages())
102 | .imageDimensions("Dimensions: " + picImage.getPixelWidth() + "x" + picImage.getPixelHeight())
103 | .bgHex(picImage.getBgColor().toHexString())
104 | .bgRgb(picImage.getBgColor().toRGBString())
105 | .build();
106 | }
107 |
108 | private Pic readPic(File picFile) throws UnsupportedPicFormatException {
109 | return picIO.readPic(picFile);
110 | }
111 |
112 | private void writePic(File picFile, Pic pic) {
113 | new PicIO().writePic(picFile, pic);
114 | }
115 |
116 | private Image getImageFromPicImage(PicImage picImage) {
117 | return new PicImageConverter().toImage(picImage);
118 | }
119 |
120 | public void replacePicImageSprites(File file) throws WrongImageDimensionsException {
121 | Image image = imageService.openImage(file);
122 | picImageConverter.replacePicImageSprites(currentPicImage(), image);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/service/VersionService.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.service;
2 |
3 | import lombok.extern.log4j.Log4j2;
4 | import nu.xom.*;
5 | import org.springframework.stereotype.Service;
6 |
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 |
10 | @Log4j2
11 | @Service
12 | public class VersionService {
13 |
14 | private static final String VERSION_XML_PATH = "versions/versions.xml";
15 |
16 | public String getTibiaVersion(String picSignature) {
17 | String tibiaVersion = null;
18 | try {
19 | tibiaVersion = parseVersion(picSignature);
20 | }
21 | catch (ParsingException | IOException e) {
22 | log.error("Failed to create the versions.xml document");
23 | throw new RuntimeException(e);
24 | }
25 | return tibiaVersion;
26 | }
27 |
28 | private String parseVersion(String picSignature) throws ParsingException, IOException {
29 | Builder parser = new Builder();
30 | Document doc = parser.build(getVersionXmlStream());
31 | Element root = doc.getRootElement();
32 | Elements versions = root.getChildElements();
33 |
34 | for (int i = 0; i < versions.size(); i++) {
35 | String picVersion = versions.get(i).getAttributeValue("pic");
36 | if (picVersion.equals(picSignature)) {
37 | return versions.get(i).getAttributeValue("string");
38 | }
39 | }
40 |
41 | return null;
42 | }
43 |
44 | private InputStream getVersionXmlStream() {
45 | return getClass().getClassLoader().getResourceAsStream(VERSION_XML_PATH);
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/service/exceptions/UnsupportedPicFormatException.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.service.exceptions;
2 |
3 | public class UnsupportedPicFormatException extends Exception {
4 | public UnsupportedPicFormatException(String message) {
5 | super(message);
6 | }
7 | public UnsupportedPicFormatException(String message, Throwable throwable) {
8 | super(message, throwable);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/service/exceptions/WrongImageDimensionsException.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.service.exceptions;
2 |
3 | public class WrongImageDimensionsException extends Exception {
4 | public WrongImageDimensionsException(String message) {
5 | super(message);
6 | }
7 | public WrongImageDimensionsException(String message, Throwable throwable) {
8 | super(message, throwable);
9 | }
10 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/ui/FileChooserDialog.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.ui;
2 |
3 | import io.github.elime1.piceditor.application.AppPreferences;
4 | import javafx.stage.FileChooser;
5 | import javafx.stage.Window;
6 | import lombok.extern.log4j.Log4j2;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.stereotype.Service;
9 |
10 | import java.io.File;
11 |
12 | import static java.util.Objects.nonNull;
13 |
14 | @Log4j2
15 | @Service
16 | public class FileChooserDialog {
17 |
18 | private AppPreferences appPreferences;
19 |
20 | @Autowired
21 | public FileChooserDialog(AppPreferences appPreferences) {
22 | this.appPreferences = appPreferences;
23 | }
24 |
25 | public File showReplaceImageDialog(Window parent) {
26 | log.debug("Opening replace with image dialog");
27 | File imageFile = showOpenDialog("Replace with image", FileType.IMAGE, parent);
28 | if(nonNull(imageFile) && imageFile.exists()) {
29 | this.appPreferences.setImagePath(imageFile.getParent());
30 | return imageFile;
31 | }
32 | log.debug("Replace with image dialog canceled");
33 | return null;
34 | }
35 |
36 | public File showSaveImageDialog(Window parent) {
37 | log.debug("Opening save image dialog");
38 | File imageFile = showSaveDialog("Save image", FileType.IMAGE, parent);
39 | if(nonNull(imageFile)) {
40 | this.appPreferences.setImagePath(imageFile.getParent());
41 | return imageFile;
42 | }
43 | log.debug("Save image dialog canceled");
44 | return null;
45 | }
46 |
47 | public File showOpenPicDialog(Window parent) {
48 | log.debug("Opening open pic file dialog");
49 | File picFile = showOpenDialog("Open pic file", FileType.PIC, parent);
50 | if(nonNull(picFile) && picFile.exists()) {
51 | this.appPreferences.setPicPath(picFile.getParent());
52 | return picFile;
53 | }
54 | log.debug("Open pic file dialog canceled");
55 | return null;
56 | }
57 |
58 | public File showSavePicDialog(Window parent) {
59 | log.debug("Opening save pic dialog");
60 | File picFile = showSaveDialog("Save pic", FileType.PIC, parent);
61 | if(nonNull(picFile)) {
62 | this.appPreferences.setPicPath(picFile.getParent());
63 | return picFile;
64 | }
65 | log.debug("Save pic dialog canceled");
66 | return null;
67 | }
68 |
69 | private File showOpenDialog(String header, FileType extType, Window parent) {
70 | return createFileChooser(header, extType).showOpenDialog(parent);
71 | }
72 |
73 | private File showSaveDialog(String header, FileType extType, Window parent) {
74 | return createFileChooser(header, extType).showSaveDialog(parent);
75 | }
76 |
77 | private FileChooser createFileChooser(String title, FileType fileType) {
78 | FileChooser fileChooser = new FileChooser();
79 | String path = null;
80 | switch (fileType) {
81 | case IMAGE:
82 | fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("Image", "*.png"));
83 | fileChooser.setInitialFileName("image.png");
84 | path = appPreferences.getImagePath();
85 | break;
86 | case PIC:
87 | fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("Pic File", "*.pic"));
88 | fileChooser.setInitialFileName("Tibia.pic");
89 | path = appPreferences.getPicPath();
90 | }
91 | log.debug("Use saved path as initial directory: " + path);
92 | File initialDirectory = new File(path);
93 | if (!initialDirectory.exists()) {
94 | String userHomeDir = System.getProperty("user.home");
95 | initialDirectory = new File(userHomeDir);
96 | log.debug("Path is invalid - Using user home directory instead: " + userHomeDir);
97 | }
98 | fileChooser.setInitialDirectory(initialDirectory);
99 | fileChooser.setTitle(title);
100 | return fileChooser;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/ui/FileType.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.ui;
2 |
3 | enum FileType {
4 | IMAGE,
5 | PIC
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/utils/ImageIndex.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.utils;
2 |
3 | public class ImageIndex {
4 | private int numberOfImages;
5 | private int currentIndex;
6 |
7 | public ImageIndex(int numberOfImages, int currentIndex) {
8 | this.numberOfImages = numberOfImages;
9 | this.currentIndex = currentIndex;
10 | }
11 |
12 | public int currentIndex() {
13 | return currentIndex;
14 | }
15 |
16 | public int currentImageNumber() {
17 | return currentIndex + 1;
18 | }
19 |
20 | public void setCurrentIndex(int index) {
21 | if (index >= 0 && index < numberOfImages) {
22 | currentIndex = index;
23 | }
24 | }
25 |
26 | public int nexImage() {
27 | if (isLast(currentIndex)) {
28 | return currentIndex = 0;
29 | }
30 | return ++currentIndex;
31 | }
32 |
33 | public int previousImage() {
34 | if (isFirst(currentIndex)) {
35 | return currentIndex = numberOfImages - 1;
36 | }
37 | return --currentIndex;
38 | }
39 |
40 | private boolean isFirst(int index) {
41 | return index == 0;
42 | }
43 |
44 | private boolean isLast(int index) {
45 | return index == numberOfImages - 1;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/utils/PicIO.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.utils;
2 |
3 | import io.github.elime1.piceditor.models.Pic;
4 | import io.github.elime1.piceditor.models.PicColor;
5 | import io.github.elime1.piceditor.models.PicImage;
6 | import io.github.elime1.piceditor.models.Sprite;
7 | import io.github.elime1.piceditor.service.exceptions.UnsupportedPicFormatException;
8 | import lombok.extern.log4j.Log4j2;
9 | import org.springframework.stereotype.Component;
10 |
11 | import java.io.File;
12 |
13 | @Log4j2
14 | @Component
15 | public class PicIO {
16 |
17 | private static final int OLD_TIBIA_SIGNATURE = 0x1fd0302; //Before 7.0
18 |
19 | public Pic readPic(File picFile) throws UnsupportedPicFormatException {
20 |
21 | log.info("Opening pic from file: " + picFile.getPath());
22 |
23 | ReadableDataBuffer buffer = new ReadableDataBuffer(picFile);
24 |
25 | Pic pic = new Pic();
26 |
27 | pic.setNumberOfBytes(buffer.getNumberOfBytes());
28 |
29 | log.debug("Start reading pic...");
30 |
31 | pic.setSignature((int) buffer.getU32());
32 |
33 | if (pic.getSignature() == OLD_TIBIA_SIGNATURE) {
34 | log.debug("User tried to open an old pic file.");
35 | throw new UnsupportedPicFormatException("The Tibia.pic file is of old unsupported format. (Older than Tibia 7.0)");
36 | }
37 |
38 | pic.setNumberOfImages(buffer.getU16());
39 |
40 | log.debug("Signature: " + Integer.toHexString(pic.getSignature()));
41 | log.debug("Number of images: " + pic.getNumberOfImages());
42 |
43 | PicImage[] picImages = new PicImage[pic.getNumberOfImages()];
44 |
45 | for (int i = 0; i < picImages.length; i++) {
46 | log.debug("Extracting image " + (i + 1));
47 | PicImage picImage = new PicImage(pic);
48 |
49 | //Number of sprites wide
50 | picImage.setWidth(buffer.getU8());
51 | //Number of sprites high
52 | picImage.setHeight(buffer.getU8());
53 |
54 | //Transparent color pixels
55 | PicColor bgColor = new PicColor(buffer.getByte(), buffer.getByte(), buffer.getByte());
56 | picImage.setBgColor(bgColor);
57 |
58 | int numberOfSprites = picImage.getWidth() * picImage.getHeight();
59 |
60 | Sprite[] sprites = new Sprite[numberOfSprites];
61 |
62 | for (int j = 0; j < numberOfSprites; j++) {
63 |
64 | int spritePos = (int) buffer.getU32(); //Read the sprites position
65 | int storedPos = buffer.position(); //Store our current position
66 | buffer.position(spritePos); //Jump to where the sprite is stored
67 |
68 | //Save sprite data
69 | sprites[j] = new Sprite(buffer.getBytes(buffer.getU16()));
70 |
71 | buffer.position(storedPos); //Jump back to previous position
72 | }
73 |
74 | picImage.setSprites(sprites);
75 | picImages[i] = picImage;
76 | }
77 |
78 | pic.setPicImages(picImages);
79 |
80 | log.debug("Done reading pic");
81 |
82 | return pic;
83 | }
84 |
85 | public void writePic(File picFile, Pic pic) {
86 |
87 | WritableDataBuffer buffer = new WritableDataBuffer(pic.getNumberOfBytes());
88 |
89 | log.debug("Start compiling pic...");
90 |
91 | buffer.putU32(pic.getSignature());
92 |
93 | buffer.putU16(pic.getNumberOfImages());
94 |
95 | PicImage[] picImages = pic.getPicImages();
96 |
97 | //Calculate where we can put the first sprite
98 | int spritePos = buffer.position();
99 | for (PicImage picImage : picImages) {
100 | spritePos += 5 + picImage.getWidth() * picImage.getHeight() * Integer.BYTES;
101 | }
102 |
103 | for (int i = 0; i < picImages.length; i++) {
104 | log.debug("Compiling image " + (i + 1));
105 |
106 | buffer.putU8(picImages[i].getWidth());
107 | buffer.putU8(picImages[i].getHeight());
108 | PicColor bgColor = picImages[i].getBgColor();
109 | buffer.putByte(bgColor.getR());
110 | buffer.putByte(bgColor.getG());
111 | buffer.putByte(bgColor.getB());
112 |
113 | Sprite[] sprites = picImages[i].getSprites();
114 | log.debug("The image consists of " + sprites.length + " sprites");
115 | for (Sprite sprite : sprites) {
116 | buffer.putU32(spritePos);
117 |
118 | int pos = buffer.position();
119 | buffer.position(spritePos);
120 |
121 | int spriteOffsetPos = buffer.position();
122 | buffer.skip(Short.BYTES);
123 |
124 | buffer.putBytes(sprite.getPixelData());
125 |
126 | buffer.putU16(spriteOffsetPos, buffer.position() - spritePos - Short.BYTES);
127 | spritePos = buffer.position();
128 | buffer.position(pos);
129 | }
130 | }
131 |
132 | log.info("Saving pic to file: " + picFile.getPath());
133 | buffer.writeFile(picFile);
134 |
135 | log.debug("Done compiling pic");
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/utils/PicImageConverter.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.utils;
2 |
3 | import io.github.elime1.piceditor.models.PicColor;
4 | import io.github.elime1.piceditor.models.PicImage;
5 | import io.github.elime1.piceditor.models.Sprite;
6 | import io.github.elime1.piceditor.service.exceptions.WrongImageDimensionsException;
7 | import javafx.scene.image.Image;
8 | import javafx.scene.image.PixelReader;
9 | import javafx.scene.image.PixelWriter;
10 | import javafx.scene.image.WritableImage;
11 | import lombok.extern.log4j.Log4j2;
12 | import org.springframework.stereotype.Component;
13 |
14 | @Log4j2
15 | @Component
16 | public class PicImageConverter {
17 |
18 | public Image toImage(PicImage picImage) {
19 | Sprite[] sprites = picImage.getSprites();
20 |
21 | int width = picImage.getWidth();
22 | int height = picImage.getHeight();
23 | int size = Sprite.DEFAULT_W_H;
24 |
25 | WritableImage writableImage = new WritableImage(size * width, size * height);
26 | PixelWriter pixelWriter = writableImage.getPixelWriter();
27 | PicColor bgColor = picImage.getBgColor();
28 |
29 | int spriteIndex = 0;
30 | for (int h = 0; h < height; h++) {
31 | for (int w = 0; w < width; w++) {
32 | byte[] pixelData = sprites[spriteIndex].getPixelData();
33 | int currentPixel = 0;
34 | int i = 0;
35 | while (i < pixelData.length) {
36 | int backgroundPixels = ((pixelData[i++] & 0xff) | (pixelData[i++] & 0xff) << 8);
37 | int coloredPixels = ((pixelData[i++] & 0xff) | (pixelData[i++] & 0xff) << 8);
38 | for (int j = 0; j < backgroundPixels; j++) {
39 | pixelWriter.setArgb(
40 | size * w + currentPixel % size,
41 | size * h + currentPixel / size,
42 | rgbToArgbInt(bgColor.getR(), bgColor.getG(), bgColor.getB())
43 | );
44 | currentPixel++;
45 | }
46 | for (int j = 0; j < coloredPixels; j++) {
47 | pixelWriter.setArgb(
48 | size * w + currentPixel % size,
49 | size * h + currentPixel / size,
50 | rgbToArgbInt(pixelData[i++], pixelData[i++], pixelData[i++])
51 | );
52 | currentPixel++;
53 | }
54 | }
55 | spriteIndex++;
56 | }
57 | }
58 | return writableImage;
59 | }
60 |
61 | public void replacePicImageSprites(PicImage picImage, Image image) throws WrongImageDimensionsException {
62 |
63 | int width = (int) image.getWidth() / Sprite.DEFAULT_W_H;
64 | int height = (int) image.getHeight() / Sprite.DEFAULT_W_H;
65 |
66 | if (width != picImage.getWidth() || height != picImage.getHeight()) {
67 | String msg = "The image must have the same dimensions as the image it is replacing. (" +
68 | picImage.getPixelWidth() + "x" + picImage.getPixelHeight() + " pixels)";
69 | log.warn("Replace image failed - " + "The image must have the same dimensions as the image it is replacing.");
70 | throw new WrongImageDimensionsException(msg);
71 | }
72 |
73 | PicColor bgColor = picImage.getBgColor();
74 | Sprite[] sprites = new Sprite[width * height];
75 |
76 | PixelReader pixelReader = image.getPixelReader();
77 |
78 | int nSpritePixels = Sprite.DEFAULT_W_H * Sprite.DEFAULT_W_H;
79 | int spriteIndex = 0;
80 | for (int h = 0; h < height; h++) {
81 | for (int w = 0; w < width; w++) {
82 | WritableDataBuffer buffer = new WritableDataBuffer(3584);
83 | int currentPixel = 0;
84 | int backgroundPixels = 0;
85 | int coloredPixels = 0;
86 |
87 | while (currentPixel < nSpritePixels) {
88 | int backgroundPixelsPos = buffer.position();
89 | buffer.skip(Short.BYTES);
90 | int coloredPixelsPos = buffer.position();
91 | buffer.skip(Short.BYTES);
92 |
93 | while (currentPixel < nSpritePixels) {
94 | int argb = pixelReader.getArgb(
95 | Sprite.DEFAULT_W_H * w + currentPixel % Sprite.DEFAULT_W_H,
96 | Sprite.DEFAULT_W_H * h + currentPixel / Sprite.DEFAULT_W_H
97 | );
98 |
99 | if (((argb >> 24) & 0xFF) == 0) {
100 | currentPixel++;
101 | break;
102 | }
103 |
104 | if (bgColor.equals(argbIntToPicColor(argb))) {
105 | backgroundPixels++;
106 | } else {break;}
107 | currentPixel++;
108 | }
109 |
110 | while (currentPixel < nSpritePixels) {
111 | int argb = pixelReader.getArgb(
112 | Sprite.DEFAULT_W_H * w + currentPixel % Sprite.DEFAULT_W_H,
113 | Sprite.DEFAULT_W_H * h + currentPixel / Sprite.DEFAULT_W_H
114 | );
115 |
116 | if (((argb >> 24) & 0xFF) == 0) {
117 | currentPixel++;
118 | break;
119 | }
120 |
121 | PicColor color = argbIntToPicColor(argb);
122 |
123 | if (!bgColor.equals(color)) {
124 | buffer.putByte(color.getR());
125 | buffer.putByte(color.getG());
126 | buffer.putByte(color.getB());
127 | coloredPixels++;
128 | } else {break;}
129 | currentPixel++;
130 | }
131 |
132 | buffer.putU16(backgroundPixelsPos, backgroundPixels);
133 | buffer.putU16(coloredPixelsPos, coloredPixels);
134 | coloredPixels = 0;
135 | backgroundPixels = 0;
136 | }
137 |
138 | sprites[spriteIndex] = new Sprite(buffer.array());
139 | spriteIndex++;
140 | }
141 | }
142 |
143 | picImage.setSprites(sprites);
144 | picImage.setWidth(width);
145 | picImage.setHeight(height);
146 | }
147 |
148 | private int rgbToArgbInt(byte r, byte g, byte b) {
149 | return 0xFF << 24 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | b & 0xFF;
150 | }
151 |
152 | private PicColor argbIntToPicColor(int argb) {
153 | return new PicColor((byte) ((argb >> 16) & 0xFF), (byte) ((argb >> 8) & 0xFF), (byte) (argb & 0xFF));
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/utils/ReadableDataBuffer.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.utils;
2 |
3 | import org.apache.logging.log4j.LogManager;
4 | import org.apache.logging.log4j.Logger;
5 |
6 | import java.io.*;
7 | import java.nio.ByteBuffer;
8 | import java.nio.ByteOrder;
9 | import java.util.Arrays;
10 |
11 | public class ReadableDataBuffer {
12 |
13 | private static Logger log = LogManager.getLogger();
14 |
15 | private ByteBuffer byteBuffer;
16 |
17 | public ReadableDataBuffer(File picFile) {
18 | loadFile(picFile);
19 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
20 | }
21 |
22 | public int getNumberOfBytes() {
23 | return byteBuffer.limit();
24 | }
25 |
26 | public void rewind() {
27 | byteBuffer.rewind();
28 | }
29 |
30 | public void position(int newPosition) {
31 | byteBuffer.position(newPosition);
32 | }
33 |
34 | public int position() {
35 | return byteBuffer.position();
36 | }
37 |
38 | public long getU32() {
39 | return byteBuffer.getInt() & 0xffffffffl;
40 | }
41 |
42 | public int getU16() {
43 | return byteBuffer.getChar();
44 | }
45 |
46 | public int getU8() {
47 | return byteBuffer.get() & 0xFF;
48 | }
49 |
50 | public byte getByte() {
51 | return byteBuffer.get();
52 | }
53 |
54 | public byte[] getBytes(int nBytes) {
55 | byte[] bytes = new byte[nBytes];
56 | byteBuffer.get(bytes);
57 | return bytes;
58 | }
59 |
60 | public String getString(int bytes) {
61 | byte[] b = new byte[bytes];
62 | for (int i = 0; i < bytes; i++) {
63 | b[i] = byteBuffer.get();
64 | }
65 | return Arrays.toString(b);
66 | }
67 |
68 | public void skip(int nBytes) {
69 | byteBuffer.position(byteBuffer.position() + nBytes);
70 | }
71 |
72 | private void loadFile(File picFile) {
73 | FileInputStream inputStream = null;
74 | try {
75 | inputStream = new FileInputStream(picFile);
76 |
77 | byte[] data = new byte[(int)picFile.length()];
78 | inputStream.read(data);
79 |
80 | this.byteBuffer = byteBuffer.wrap(data);
81 |
82 | } catch (FileNotFoundException e) {
83 | log.warn("Could not open the file: " + picFile.getPath());
84 | throw new RuntimeException(e);
85 | } catch (IOException e) {
86 | log.warn("Faild while reading file: " + picFile.getPath());
87 | throw new RuntimeException(e);
88 | } finally {
89 | try {
90 | if (inputStream != null) inputStream.close();
91 | } catch (IOException e) {
92 | log.warn("Failed to close FileInputStream");
93 | throw new RuntimeException(e);
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/java/io/github/elime1/piceditor/utils/WritableDataBuffer.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor.utils;
2 |
3 | import org.apache.logging.log4j.LogManager;
4 | import org.apache.logging.log4j.Logger;
5 |
6 | import java.io.File;
7 | import java.io.FileNotFoundException;
8 | import java.io.FileOutputStream;
9 | import java.io.IOException;
10 | import java.nio.ByteBuffer;
11 | import java.nio.ByteOrder;
12 | import java.util.Arrays;
13 |
14 | public class WritableDataBuffer {
15 |
16 | private static Logger log = LogManager.getLogger();
17 |
18 | private ByteBuffer byteBuffer;
19 |
20 | public WritableDataBuffer(int size) {
21 | byteBuffer = ByteBuffer.allocate(size);
22 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
23 | }
24 |
25 | public void rewind() {
26 | byteBuffer.rewind();
27 | }
28 |
29 | public void position(int newPosition) {
30 | byteBuffer.position(newPosition);
31 | }
32 |
33 | public void skip(int nBytes) {
34 | byteBuffer.position(byteBuffer.position() + nBytes);
35 | }
36 |
37 | public int position() {
38 | return byteBuffer.position();
39 | }
40 |
41 | public void putByte(byte value) {
42 | byteBuffer.put(value);
43 | }
44 |
45 | public void putU8(int value) {
46 | byteBuffer.put((byte)value);
47 | }
48 |
49 | public void putU16(int value) {
50 | byteBuffer.putChar((char) value);
51 | }
52 |
53 | public void putU16(int index, int value) {
54 | byteBuffer.putChar(index, (char) value);
55 | }
56 |
57 | public void putU32(long value) {
58 | byteBuffer.putInt((int) value);
59 | }
60 |
61 | public void putBytes(byte[] bytes) {
62 | byteBuffer.put(bytes);
63 | }
64 |
65 | public byte[] array() {
66 | byte[] bufferArray = byteBuffer.array();
67 | return Arrays.copyOf(bufferArray, byteBuffer.position());
68 | }
69 |
70 |
71 | public void writeFile(File picFile) {
72 | FileOutputStream outputStream = null;
73 |
74 | try {
75 | outputStream = new FileOutputStream(picFile);
76 | outputStream.write(byteBuffer.array());
77 | } catch (FileNotFoundException e) {
78 | log.warn("Could not open file: " + picFile.getPath());
79 | throw new RuntimeException(e);
80 | } catch (IOException e) {
81 | log.warn("Failed while writing to file: " + picFile.getPath());
82 | throw new RuntimeException(e);
83 | } finally {
84 | try {
85 | if (outputStream != null) outputStream.close();
86 | } catch (IOException e) {
87 | log.warn("Failed to close FileOutputStream");
88 | throw new RuntimeException(e);
89 | }
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/src/main/resources/css/style.css:
--------------------------------------------------------------------------------
1 |
2 | .scroll-pane {
3 | -fx-background-color:transparent;
4 | -fx-border-color: rgb(23, 26, 35);
5 | }
6 |
7 | .content-panel {
8 | -fx-background: rgb(40, 43, 52);
9 | -fx-background-color: rgb(33, 36, 43);
10 | }
11 |
12 | .side-panel {
13 | -fx-background-color: rgb(33, 36, 43);
14 | -fx-control-inner-background: rgb(23, 26, 35);
15 | }
16 |
17 | .app-label {
18 | -fx-font-family: "Verdana";
19 | -fx-text-fill: rgb(168, 168, 168);;
20 | }
21 |
22 | .imageBackground {
23 | -fx-border-width:0;
24 | }
25 |
26 | .separator {
27 | -fx-background: rgb(23, 26, 35);
28 | }
29 |
30 | .left-side-button {
31 | -fx-background-color: rgb(40, 43, 52);
32 | -fx-text-fill: rgb(168, 168, 168);
33 | -fx-font-family: "Verdana";
34 | -fx-border-color: rgb(23, 26, 35);
35 | -fx-border-width: 1px;
36 | }
37 |
38 | .left-side-button:hover {
39 | -fx-text-fill: #fafafa;
40 | -fx-background-color: rgb(50, 53, 62);
41 | }
42 |
43 | .next-image-button {
44 | -fx-background-color: rgb(50, 53, 62);
45 | -fx-border-width: 1px;
46 | -fx-border-color: rgb(23, 26, 35);
47 | -fx-shape: "M 240.54492 113.77344 L 240.54492 178.07617 L 100 178.07617 L 100 275.21875 L 240.54492 275.21875 L 240.54492 339.52344 L 418.9082 226.64844 L 240.54492 113.77344 z";
48 | }
49 |
50 | .previous-image-button {
51 | -fx-background-color: rgb(50, 53, 62);
52 | -fx-border-width: 1px;
53 | -fx-border-color: rgb(23, 26, 35);
54 | -fx-shape: "m 278.36328,113.77344 0,64.30273 140.54492,0 0,97.14258 -140.54492,0 0,64.30469 L 100,226.64844 278.36328,113.77344 Z";
55 | }
56 |
57 | .next-image-button:hover, .previous-image-button:hover{
58 | -fx-background-color: rgb(60, 63, 72);
59 | }
60 |
61 | .progress-bar .track {
62 | -fx-background-radius: 0;
63 | -fx-background-insets: 0;
64 | -fx-background-color: rgb(33, 36, 43);
65 | }
66 |
67 | .progress-bar .bar {
68 | -fx-padding: 0.2;
69 | -fx-background-color: rgb(50,50,255);
70 | -fx-background-radius: 0;
71 | -fx-background-insets: 0;
72 | }
73 |
74 | .info-dialog {
75 | -fx-alignment: center;
76 | }
77 |
78 | /****************************************************************
79 |
80 | ScrollBar Skin
81 |
82 | ****************************************************************/
83 | .scroll-bar{
84 | -fx-background-color: rgb(96,96,96);
85 | -fx-background-radius: 2em;
86 | }
87 | .scroll-bar:horizontal .track,
88 | .scroll-bar:vertical .track {
89 | -fx-background-color: transparent;
90 | -fx-border-color:transparent;
91 | -fx-background-radius: 2em;
92 | }
93 | .scroll-bar:vertical .track-background,
94 | .scroll-bar:horizontal .track-background {
95 | -fx-background-color: transparent;
96 | -fx-background-insets: 0;
97 | -fx-background-radius: 2em;
98 | }
99 | .scroll-bar:horizontal .thumb {
100 | -fx-background-color: rgb(211,211,211);
101 | -fx-background-insets: 4 0 4 0;
102 | -fx-background-radius: 2em;
103 | }
104 | .scroll-bar:vertical .thumb {
105 | -fx-background-color: rgb(211,211,211);
106 | -fx-background-insets: 0 4 0 4;
107 | -fx-background-radius: 2em;
108 | }
109 | .scroll-bar:horizontal .thumb:hover,
110 | .scroll-bar:vertical .thumb:hover {
111 | -fx-background-color: rgb(231,231,231);
112 | }
113 | .scroll-bar:horizontal .thumb:pressed,
114 | .scroll-bar:vertical .thumb:pressed {
115 | -fx-background-color: rgb(255,255,255);
116 | }
117 | .scroll-bar:vertical .increment-button, .scroll-bar:vertical .decrement-button {
118 | -fx-background-color:transparent;
119 | -fx-background-radius: 2em;
120 | -fx-padding: 5;
121 | }
122 | .scroll-bar:horizontal .increment-button, .scroll-bar:horizontal .decrement-button {
123 | -fx-background-color:transparent;
124 | -fx-background-radius: 2em;
125 | -fx-padding: 5;
126 | }
127 | .scroll-bar:horizontal .increment-arrow {
128 | -fx-shape: "M 0 0 L 4 8 L 8 0 Z";
129 | -fx-background-color: rgb(211,211,211);
130 | -fx-padding: 0.25em;
131 | -fx-rotate: -90;
132 | }
133 | .scroll-bar:vertical .increment-arrow {
134 | -fx-background-color: rgb(211,211,211);
135 | -fx-shape: "M 0 0 L 4 8 L 8 0 Z";
136 | -fx-padding: 0.25em;
137 | -fx-rotate: 0;
138 | }
139 | .scroll-bar:horizontal .decrement-arrow {
140 | -fx-background-color: rgb(211,211,211);
141 | -fx-shape: "M 0 0 L 4 8 L 8 0 Z";
142 | -fx-padding: 0.25em;
143 | -fx-rotate: 90;
144 | }
145 | .scroll-bar:vertical .decrement-arrow {
146 | -fx-background-color: rgb(211,211,211);
147 | -fx-shape: "M 0 0 L 4 8 L 8 0 Z";
148 | -fx-padding: 0.25em;
149 | -fx-rotate: -180;
150 | }
151 | .scroll-bar:vertical:focused,
152 | .scroll-bar:horizontal:focused {
153 | -fx-background-color: transparent,rgb(96,96,96),rgb(96,96,96);
154 | }
155 |
156 | .corner {
157 | -fx-background-color: transparent;
158 | }
--------------------------------------------------------------------------------
/src/main/resources/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Elime1/pic-editor/64d1bb3267e985a0da993871182fad45f5a73ba0/src/main/resources/img/icon.png
--------------------------------------------------------------------------------
/src/main/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/main/resources/versions/versions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/main/resources/view/main.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
63 |
64 |
65 |
66 |
74 |
75 |
76 |
77 |
78 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/src/test/java/io/github/elime1/piceditor/PicServiceTest.java:
--------------------------------------------------------------------------------
1 | package io.github.elime1.piceditor;
2 |
3 | import io.github.elime1.piceditor.application.spring.SpringConfig;
4 | import io.github.elime1.piceditor.models.Pic;
5 | import io.github.elime1.piceditor.service.ImageService;
6 | import io.github.elime1.piceditor.service.PicService;
7 | import io.github.elime1.piceditor.service.VersionService;
8 | import io.github.elime1.piceditor.service.exceptions.UnsupportedPicFormatException;
9 | import io.github.elime1.piceditor.utils.PicIO;
10 | import io.github.elime1.piceditor.utils.PicImageConverter;
11 | import org.junit.Before;
12 | import org.junit.Test;
13 | import org.junit.runner.RunWith;
14 | import org.mockito.InjectMocks;
15 | import org.mockito.Mock;
16 | import org.mockito.MockitoAnnotations;
17 | import org.springframework.beans.factory.annotation.Autowired;
18 | import org.springframework.test.context.ContextConfiguration;
19 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
20 |
21 | import static org.mockito.Mockito.when;
22 | import static org.springframework.test.util.AssertionErrors.assertEquals;
23 |
24 | @RunWith(SpringJUnit4ClassRunner.class)
25 | @ContextConfiguration(classes = SpringConfig.class)
26 | public class PicServiceTest {
27 |
28 | @Mock
29 | private PicIO picIO;
30 | @Mock
31 | private PicImageConverter picImageConverter;
32 | @Mock
33 | private VersionService versionService;
34 | @Mock
35 | private ImageService imageService;
36 |
37 | @InjectMocks
38 | @Autowired
39 | private PicService picService;
40 |
41 | @Before
42 | public void setup() throws UnsupportedPicFormatException {
43 | MockitoAnnotations.initMocks(this);
44 | }
45 |
46 | @Test
47 | public void currentlyDisplayedImageNumberTest() throws UnsupportedPicFormatException {
48 |
49 | Pic pic = new Pic();
50 | pic.setNumberOfImages(5);
51 |
52 | when(picIO.readPic(null)).thenReturn(pic);
53 |
54 | picService.loadPic(null);
55 | picService.previousImage();
56 | picService.previousImage();
57 |
58 | assertEquals("Wrong image number", 4, picService.getCurrentImageNumber());
59 |
60 | picService.nextImage();
61 | picService.nextImage();
62 | picService.nextImage();
63 |
64 | assertEquals("Wrong image number", 2, picService.getCurrentImageNumber());
65 | }
66 | }
67 |
--------------------------------------------------------------------------------