├── README.md ├── converter ├── build.gradle └── src │ ├── LICENSE │ ├── LICENSE.jcommander │ └── pl │ └── asie │ └── ctif │ ├── Converter.java │ ├── Main.java │ ├── PaletteGeneratorKMeans.java │ ├── Utils.java │ ├── colorspace │ ├── Colorspace.java │ └── ColorspaceMatrix.java │ └── platform │ ├── Platform.java │ ├── PlatformComputerCraft.java │ ├── PlatformOpenComputers.java │ └── PlatformZXSpectrum.java └── viewers ├── ctif-cc-1.8.lua ├── ctif-cc.lua └── oc └── ctifview.lua /README.md: -------------------------------------------------------------------------------- 1 | # CTIF 2 | 3 | An image format for OpenComputers and ComputerCraft. See releases for JAR converter binaries. 4 | 5 | * [OpenComputers Thread/Examples](https://oc.cil.li/index.php?/topic/864-chenthread-image-format-high-quality-images-on-opencomputers/) 6 | * [ComputerCraft Thread/Examples](http://www.computercraft.info/forums2/index.php?/topic/26186-chenthread-image-format-quality-images-on-18-computercraft) 7 | 8 | # Usage 9 | 10 | To run CTIFConverter, you need Java 8. 11 | 12 | java -jar CTIFConverter.jar -h 13 | 14 | will be your best friend. Here's an example command: 15 | 16 | java -jar CTIFConverter.jar -m oc-tier3 -P preview.png -o image.ctif image.png 17 | 18 | will convert image.png to image.ctif and kindly save preview.png as a preview file. (The "-P preview.png" can be omitted) 19 | 20 | java -jar CTIFConverter.jar -m cc -W 102 -H 57 -o image.ctif image.png 21 | 22 | will convert your image into a ComputerCraft picture of at most 102x57. If you want to ignore the aspect ratio and force it to be 23 | exactly 102x57, use "-N". 24 | 25 | ## Note when running on Windows 26 | 27 | Download **imagemagick** for java, then add the path to an env variable called `IM4JAVA_TOOLPATH` 28 | 29 | # Viewers 30 | 31 | If you just want to view CTIF files, see the viewers directory. 32 | 33 | * ctif-cc.lua - ComputerCraft image viewer. "ctif-cc {file} [monitor side]" to use. If it errors about the image size being too 34 | large, keep in mind it operates on *characters*, while the converter operates on *pixels* - to convert from one to the other, 35 | multiply the width by 2 and the height by 3. 36 | 37 | * ctif-oc.lua - OpenComputers image viewer. Not optimized - expect it to load images slower than what you saw at BTM. Requires Lua 5.3. 38 | 39 | An optimized OpenComputers viewer will be released as part of the promised-at-BTM15 release of OpenPoint soon. 40 | 41 | ## OpenComputers note 42 | 43 | To run this, you need to set the `CPU`'s architecture to **Lua 5.3**, to do this, just *sneak-click the cpu while holding it*. 44 | -------------------------------------------------------------------------------- /converter/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'idea' 4 | id 'com.github.johnrengelman.shadow' version '7.0.0' 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | implementation 'org.im4java:im4java:1.4.0' 13 | implementation 'com.beust:jcommander:1.82' 14 | } 15 | 16 | sourceSets { 17 | main { 18 | java.srcDirs = ['src'] 19 | } 20 | } 21 | 22 | jar { 23 | manifest { 24 | attributes 'Main-Class': 'pl.asie.ctif.Main' 25 | } 26 | } 27 | 28 | build.dependsOn(shadowJar); 29 | -------------------------------------------------------------------------------- /converter/src/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 Adrian Siekierka 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /converter/src/LICENSE.jcommander: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012, Cedric Beust 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/Converter.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif; 2 | 3 | import pl.asie.ctif.platform.PlatformComputerCraft; 4 | import pl.asie.ctif.platform.PlatformOpenComputers; 5 | import pl.asie.ctif.platform.PlatformZXSpectrum; 6 | 7 | import java.awt.*; 8 | import java.awt.image.BufferedImage; 9 | import java.io.IOException; 10 | import java.io.OutputStream; 11 | 12 | public class Converter { 13 | public enum DitherMode { 14 | NONE, 15 | ERROR, 16 | ORDERED 17 | }; 18 | 19 | private final Color[] palette; 20 | private final BufferedImage image; 21 | private final DitherMode ditherMode; 22 | private final float[] ditherMatrix; 23 | private final int ditherMatrixSize, ditherMatrixOffset; 24 | private final float[][] img; 25 | private final float[][] pal; 26 | private final int cw, ch, pw, ph; 27 | private final int ditherMax; 28 | 29 | public Converter(Color[] colors, BufferedImage image, DitherMode ditherMode, float[] ditherMatrix) { 30 | int i = 0; 31 | 32 | this.ditherMode = ditherMode; 33 | this.ditherMatrix = ditherMatrix; 34 | if (ditherMode == DitherMode.ORDERED) { 35 | assert ditherMatrix != null; 36 | this.ditherMatrixSize = (int) Math.sqrt(ditherMatrix.length - 1); 37 | this.ditherMatrixOffset = 0; 38 | this.ditherMax = (int) ditherMatrix[ditherMatrix.length - 1]; 39 | } else { 40 | this.ditherMatrixSize = ditherMatrix != null ? (int) Math.sqrt(ditherMatrix.length) : 0; 41 | this.ditherMatrixOffset = (ditherMatrixSize - 1) / 2; 42 | this.ditherMax = 0; 43 | } 44 | 45 | this.image = image; 46 | this.palette = colors; 47 | 48 | this.img = new float[image.getWidth() * image.getHeight()][3]; 49 | this.pal = new float[colors.length][3]; 50 | 51 | this.pw = Main.PLATFORM.getCharWidth(); 52 | this.ph = Main.PLATFORM.getCharHeight(); 53 | this.cw = image.getWidth() / pw; 54 | this.ch = image.getHeight() / ph; 55 | 56 | for (i = 0; i < img.length; i++) { 57 | img[i] = Main.COLORSPACE.fromRGB(image.getRGB(i % image.getWidth(), i / image.getWidth())); 58 | } 59 | 60 | for (i = 0; i < colors.length; i++) { 61 | pal[i] = Main.COLORSPACE.fromRGB(colors[i].getRGB()); 62 | } 63 | } 64 | 65 | public BufferedImage write(OutputStream stream) throws IOException { 66 | BufferedImage output = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_3BYTE_BGR); 67 | 68 | stream.write('C'); 69 | stream.write('T'); 70 | stream.write('I'); 71 | stream.write('F'); 72 | 73 | stream.write(1); // Header version 74 | stream.write(0); // Platform variant (0 - default) 75 | stream.write(Main.PLATFORM.platformId); 76 | stream.write(Main.PLATFORM.platformId >> 8); // Platform ID 77 | stream.write(cw & 0xFF); 78 | stream.write(cw >> 8); // Width in chars 79 | stream.write(ch & 0xFF); 80 | stream.write(ch >> 8); // Height in chars 81 | stream.write(pw); // Char width 82 | stream.write(ph); // Char height 83 | 84 | stream.write(palette.length > 16 ? 8 : 4); // BPP (byte) 85 | 86 | if (Main.PLATFORM.getCustomColorCount() > 0) { 87 | stream.write(3); // Palette entry size 88 | stream.write(16); 89 | stream.write(0); // Palette array size 90 | for (int i = 0; i < 16; i++) { 91 | stream.write(palette[i].getRGB() & 0xFF); 92 | stream.write((palette[i].getRGB() >> 8) & 0xFF); 93 | stream.write((palette[i].getRGB() >> 16) & 0xFF); 94 | } 95 | } else { 96 | stream.write(0); // Palette array size 97 | stream.write(0); 98 | stream.write(0); // Palette entry size 99 | } 100 | 101 | writePixelData(stream, output); 102 | stream.close(); 103 | return output; 104 | } 105 | 106 | private void addQuantError(float[][] pixelArray, int x, int y, int w, int h, float[] expected, float[] received, float mul) { 107 | if (x >= 0 && y >= 0 && x < w && y < h) { 108 | Utils.addQuantError(pixelArray[y*w+x], expected, received, mul); 109 | } 110 | } 111 | 112 | private void writePixelData(OutputStream stream, BufferedImage output) throws IOException { 113 | int ew = (pw + ditherMatrixOffset * 2); 114 | int eh = (ph + ditherMatrixOffset * 2); 115 | 116 | int quadrantLen = (pw * ph + 7) / 8; 117 | 118 | float[][] pixels = new float[pw * ph][]; 119 | float[][] bcea = new float[ew * eh][3]; 120 | float[][] tPixels = new float[pixels.length][3]; 121 | float[][] errors = new float[ew * eh][3]; 122 | int[] bcq = new int[quadrantLen]; 123 | int[] cq = new int[quadrantLen]; 124 | 125 | float[] colA = new float[3]; 126 | 127 | boolean usePalMap = Main.OPTIMIZATION_LEVEL > 0 && Main.PLATFORM instanceof PlatformOpenComputers && ((PlatformOpenComputers) Main.PLATFORM).tier == 3; 128 | int[] palMap = new int[palette.length]; 129 | int palMapLength; 130 | for (int i = 0; i < 16; i++) 131 | palMap[i] = i; 132 | int t3OffRed = Main.OPTIMIZATION_LEVEL <= 1 ? 3 : (Main.OPTIMIZATION_LEVEL == 2 ? 2 : 1); 133 | int t3OffGreen = Main.OPTIMIZATION_LEVEL <= 1 ? 3 : (Main.OPTIMIZATION_LEVEL <= 3 ? 2 : 1); 134 | int t3OffBlue = Main.OPTIMIZATION_LEVEL <= 1 ? 2 : 1; 135 | 136 | for (int cy = 0; cy < ch; cy++) { 137 | for (int cx = 0; cx < cw; cx++) { 138 | for (int py = 0; py < ph; py++) { 139 | for (int px = 0; px < pw; px++) { 140 | pixels[py * pw + px] = img[(cy * ph + py) * image.getWidth() + cx * pw + px]; 141 | } 142 | } 143 | 144 | int bci1 = 0, bci2 = 0; 145 | double bcerr = Double.MAX_VALUE; 146 | 147 | if (usePalMap) { 148 | palMapLength = 16; 149 | int[] colorsUsed = new int[palette.length]; 150 | for (int py = 0; py < ph; py++) { 151 | for (int px = 0; px < pw; px++) { 152 | int rgb = image.getRGB(cx * pw + px, cy * ph + py); 153 | int red = (((rgb >> 16) & 0xFF) * 6 / 256); 154 | int green = (((rgb >> 8) & 0xFF) * 8 / 256); 155 | int blue = ((rgb & 0xFF) * 5 / 256); 156 | for (int rr = red - t3OffRed; rr <= red + t3OffRed; rr++) 157 | for (int rg = green - t3OffGreen; rg <= green + t3OffGreen; rg++) 158 | for (int rb = blue - t3OffBlue; rb <= blue + t3OffBlue; rb++) 159 | if (rr >= 0 && rg >= 0 && rb >= 0 && rr < 6 && rg < 8 && rb < 5) { 160 | int col = 16 + rr * 40 + rg * 5 + rb; 161 | if (colorsUsed[col] == 0) { 162 | palMap[palMapLength++] = col; 163 | colorsUsed[col] = 1; 164 | } 165 | } 166 | } 167 | } 168 | } else { 169 | palMapLength = palette.length; 170 | } 171 | 172 | boolean bcqFound = false; 173 | 174 | if (ditherMode == DitherMode.NONE && Main.OPTIMIZATION_LEVEL >= 3) { 175 | int[] colors = new int[pixels.length]; 176 | int colorCount = 0; 177 | boolean[] uColors = new boolean[palette.length]; 178 | 179 | for (int i = 0; i < pixels.length; i++) { 180 | double bestDist = Double.MAX_VALUE; 181 | int bestCol = 0; 182 | 183 | for (int cim1 = 0; cim1 < palMapLength; cim1++) { 184 | int ci1 = usePalMap ? palMap[cim1] : cim1; 185 | float[] col1 = pal[ci1]; 186 | double dist = Utils.getColorDistanceSq(col1, pixels[i]); 187 | if (dist < bestDist) { 188 | bestCol = ci1; 189 | bestDist = dist; 190 | } 191 | } 192 | 193 | if (!uColors[bestCol]) { 194 | uColors[bestCol] = true; 195 | colors[colorCount++] = bestCol; 196 | } 197 | } 198 | 199 | if (colorCount <= 2) { 200 | bci1 = colors[0]; 201 | bci2 = colors[1]; 202 | 203 | for (int i = 0; i < bcq.length; i++) 204 | bcq[i] = 0; 205 | 206 | for (int i = 0; i < pixels.length; i++) { 207 | int pos = (pw * ph - 1 - i); 208 | double dist0 = Utils.getColorDistanceSq(pal[bci1], pixels[i]); 209 | double dist1 = Utils.getColorDistanceSq(pal[bci2], pixels[i]); 210 | if (dist1 < dist0) { 211 | bcq[pos >> 3] |= (1 << (pos & 7)); 212 | } 213 | } 214 | 215 | bcqFound = true; 216 | } 217 | } 218 | 219 | if (!bcqFound) { 220 | for (int cim1 = 1; cim1 < palMapLength; cim1++) { 221 | if (bcerr == 0) break; 222 | int ci1 = usePalMap ? palMap[cim1] : cim1; 223 | float[] col1 = pal[ci1]; 224 | 225 | for (int cim2 = (Main.PLATFORM instanceof PlatformZXSpectrum) ? (cim1 >= 8 ? 8 : 0) : 0; cim2 < cim1; cim2++) { 226 | if (bcerr == 0) break; 227 | int ci2 = usePalMap ? palMap[cim2] : cim2; 228 | float[] col2 = pal[ci2]; 229 | double cerr = 0; 230 | 231 | for (int i = 0; i < quadrantLen; i++) { 232 | cq[i] = 0; 233 | } 234 | 235 | if (ditherMode == DitherMode.NONE) { 236 | for (int i = 0; i < pixels.length; i++) { 237 | float[] col = pixels[i]; 238 | double cerr1 = Utils.getColorDistanceSq(col, col1); 239 | double cerr2 = Utils.getColorDistanceSq(col, col2); 240 | if (cerr2 < cerr1) { 241 | int pos = (pw * ph - 1 - i); 242 | cq[pos >> 3] |= (1 << (pos & 7)); 243 | cerr += cerr2; 244 | } else { 245 | cerr += cerr1; 246 | } 247 | 248 | if (cerr >= bcerr) 249 | break; 250 | } 251 | } else if (ditherMode == DitherMode.ERROR) { 252 | for (int i = 0; i < pixels.length; i++) { 253 | tPixels[i][0] = pixels[i][0]; 254 | tPixels[i][1] = pixels[i][1]; 255 | tPixels[i][2] = pixels[i][2]; 256 | } 257 | 258 | for (int i = 0; i < errors.length; i++) { 259 | errors[i][0] = 0; 260 | errors[i][1] = 0; 261 | errors[i][2] = 0; 262 | } 263 | 264 | for (int i = 0; i < tPixels.length; i++) { 265 | float[] col = tPixels[i]; 266 | float[] colR; 267 | double cerr1 = Utils.getColorDistanceSq(col, col1); 268 | double cerr2 = Utils.getColorDistanceSq(col, col2); 269 | if (cerr2 < cerr1) { 270 | int pos = (pw * ph - 1 - i); 271 | cq[pos >> 3] |= (1 << (pos & 7)); 272 | cerr += cerr2; 273 | colR = col2; 274 | } else { 275 | cerr += cerr1; 276 | colR = col1; 277 | } 278 | 279 | if (cerr >= bcerr) 280 | break; 281 | 282 | int qx = (i % pw); 283 | int qy = (i / pw); 284 | 285 | int ip = ditherMatrixSize * ditherMatrixOffset; 286 | for (int iy = 0; iy < ditherMatrixSize - ditherMatrixOffset; iy++) { 287 | for (int ix = -ditherMatrixOffset; ix < ditherMatrixSize - ditherMatrixOffset; ix++) { 288 | addQuantError(tPixels, qx + ix, qy + iy, pw, ph, col, colR, ditherMatrix[ip]); 289 | addQuantError(errors, qx + ix + ditherMatrixOffset, qy + iy + ditherMatrixOffset, ew, eh, col, colR, ditherMatrix[ip]); 290 | ip++; 291 | } 292 | } 293 | } 294 | } else { 295 | // http://bisqwit.iki.fi/story/howto/dither/jy/ 296 | 297 | cerr += Utils.getColorDistanceSq(col1, col2) * 0.1 * pixels.length; 298 | 299 | for (int i = 0; i < pixels.length; i++) { 300 | float[] col = pixels[i]; 301 | int qx = (i % pw); 302 | int qy = (i / pw); 303 | 304 | float jf = 305 | (col[0] * col1[0] - col[0] * col2[0] - col1[0] * col2[0] + col2[0] * col2[0] + 306 | col[1] * col1[1] - col[1] * col2[1] - col1[1] * col2[1] + col2[1] * col2[1] + 307 | col[2] * col1[2] - col[2] * col2[2] - col1[2] * col2[2] + col2[2] * col2[2]) / 308 | ((col1[0] - col2[0]) * (col1[0] - col2[0]) + 309 | (col1[1] - col2[1]) * (col1[1] - col2[1]) + 310 | (col1[2] - col2[2]) * (col1[2] - col2[2])); 311 | int birat = ditherMax - Math.round(jf * ditherMax); 312 | if (birat < 0) birat = 0; 313 | else if (birat > ditherMax) birat = ditherMax; 314 | 315 | colA[0] = (col2[0] * birat + col1[0] * (ditherMax - birat)) / ditherMax; 316 | colA[1] = (col2[1] * birat + col1[1] * (ditherMax - birat)) / ditherMax; 317 | colA[2] = (col2[2] * birat + col1[2] * (ditherMax - birat)) / ditherMax; 318 | cerr += Utils.getColorDistanceSq(col, colA); 319 | 320 | if (cerr >= bcerr) 321 | break; 322 | 323 | int threshold = (int) ditherMatrix[((cy * ph + qy) % ditherMatrixSize) * ditherMatrixSize + ((cx * pw + qx) % ditherMatrixSize)]; 324 | if (threshold < birat) { 325 | int pos = (pw * ph - 1 - i); 326 | cq[pos >> 3] |= (1 << (pos & 7)); 327 | } 328 | } 329 | } 330 | 331 | if (cerr < bcerr) { 332 | bci1 = ci1; 333 | bci2 = ci2; 334 | bcerr = cerr; 335 | if (ditherMode == DitherMode.ERROR) { 336 | for (int i = 0; i < errors.length; i++) { 337 | bcea[i][0] = errors[i][0]; 338 | bcea[i][1] = errors[i][1]; 339 | bcea[i][2] = errors[i][2]; 340 | } 341 | } 342 | for (int i = 0; i < quadrantLen; i++) { 343 | bcq[i] = cq[i]; 344 | } 345 | } 346 | } 347 | } 348 | 349 | if (ditherMode == DitherMode.ERROR) { 350 | for (int iy = 0; iy < eh; iy++) { 351 | int ry = cy * ph + iy - ditherMatrixOffset; 352 | if (ry >= 0 && ry < ch * ph) { 353 | for (int ix = 0; ix < ew; ix++) { 354 | int rx = cx * pw + ix - ditherMatrixOffset; 355 | if (rx >= 0 && rx < cw * pw) { 356 | for (int i = 0; i < 3; i++) { 357 | img[ry * cw * pw + rx][i] += bcea[iy * ew + ix][i]; 358 | } 359 | } 360 | } 361 | } 362 | } 363 | } 364 | } 365 | 366 | int[] quadrant = bcq; 367 | int bgIndex = bci1; 368 | int fgIndex = bci2; 369 | if (bgIndex == fgIndex) { 370 | for (int i = 0; i < quadrantLen; i++) quadrant[i] = 0; 371 | } 372 | 373 | if (Main.PLATFORM instanceof PlatformComputerCraft) { 374 | if ((quadrant[0] & 0x01) != 0) { 375 | int t = fgIndex; 376 | fgIndex = bgIndex; 377 | bgIndex = t; 378 | quadrant[0] ^= 0x3F; 379 | } 380 | } else if (Main.PLATFORM instanceof PlatformOpenComputers && pw * ph > 2) { 381 | if (bgIndex > fgIndex) { 382 | int t = fgIndex; 383 | fgIndex = bgIndex; 384 | bgIndex = t; 385 | quadrant[0] ^= (1 << (pw * ph)) - 1; 386 | } 387 | } 388 | 389 | if (pw * ph == 2 && quadrant[0] == 1) { 390 | int t = fgIndex; 391 | fgIndex = bgIndex; 392 | bgIndex = t; 393 | quadrant[0] = 0; 394 | } 395 | 396 | if (palette.length > 2) { 397 | if (pw * ph == 1) { 398 | stream.write(fgIndex); 399 | } else { 400 | if (palette.length > 16) { 401 | stream.write(bgIndex); 402 | stream.write(fgIndex); 403 | } else { 404 | stream.write(bgIndex << 4 | fgIndex); 405 | } 406 | } 407 | 408 | if (pw * ph > 2) { 409 | for (int i = 0; i < quadrantLen; i++) 410 | stream.write(quadrant[i]); 411 | } 412 | } else { 413 | for (int i = 0; i < quadrantLen; i++) 414 | stream.write(quadrant[i]); 415 | } 416 | 417 | for (int py = 0; py < ph; py++) { 418 | for (int px = 0; px < pw; px++) { 419 | int i = (pw * ph - 1) - (py * pw + px); 420 | 421 | if ((quadrant[i >> 3] & (1 << (i & 7))) != 0) { 422 | output.setRGB(cx * pw + px, cy * ph + py, palette[fgIndex].getRGB()); 423 | } else { 424 | output.setRGB(cx * pw + px, cy * ph + py, palette[bgIndex].getRGB()); 425 | } 426 | } 427 | } 428 | } 429 | } 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/Main.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif; 2 | 3 | import java.awt.Color; 4 | import java.awt.image.BufferedImage; 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.util.*; 9 | 10 | import com.beust.jcommander.JCommander; 11 | import com.beust.jcommander.Parameter; 12 | import pl.asie.ctif.colorspace.Colorspace; 13 | import pl.asie.ctif.platform.Platform; 14 | import pl.asie.ctif.platform.PlatformComputerCraft; 15 | import pl.asie.ctif.platform.PlatformOpenComputers; 16 | import pl.asie.ctif.platform.PlatformZXSpectrum; 17 | 18 | public class Main { 19 | private static class Parameters { 20 | @Parameter(names = {"--palette-sampling-resolution"}, description = "The sampling resolution for palette generation. 0 = full image (1/4x1/4 image in -O3+)") 21 | private int paletteSamplingResolution = 0; 22 | 23 | @Parameter(names = {"--threads"}, description = "Amount of threads to create") 24 | private int threads = Runtime.getRuntime().availableProcessors(); 25 | 26 | @Parameter(names = {"--palette-export"}, description = "File to export the palette to.") 27 | private String paletteExport; 28 | 29 | @Parameter(names = {"--palette"}, description = "File to load the palette from.") 30 | private String palette; 31 | 32 | @Parameter(names = {"-m", "--mode"}, description = "Target platform (cc, cc-paletted, oc-tier2, oc-tier3)") 33 | private String mode = "oc-tier3"; 34 | 35 | @Parameter(names = {"-O", "--optimization-level"}, description = "Optimization level [primarily 0-4]. Larger levels = less accurate but faster generated images. Default is 1.") 36 | private int optimizationLevel = 1; 37 | 38 | @Parameter(names = {"--colorspace"}, description = "Colorspace (rgb, yuv, yiq)") 39 | private String colorspace = "yiq"; 40 | 41 | @Parameter(names = {"--dither-mode"}, description = "Dither mode (none, error, ordered)") 42 | private Converter.DitherMode ditherMode = Converter.DitherMode.ERROR; 43 | 44 | @Parameter(names = {"--dither-type"}, description = "Dither type (error: floyd-steinberg, sierra-lite; ordered: 2x2, 4x4, 8x8)") 45 | private String ditherType; 46 | 47 | @Parameter(names = {"--dither-level"}, description = "Dither level for error-type dither. 0 = off, 1 = full (default)") 48 | private float ditherLevel = 1.0f; 49 | 50 | @Parameter(names = {"-d", "--debug"}, description = "Enable debugging", hidden = true) 51 | private boolean debug = false; 52 | 53 | @Parameter(names = {"-W", "--width"}, description = "Output image width") 54 | private int w; 55 | 56 | @Parameter(names = {"-H", "--height"}, description = "Output image height") 57 | private int h; 58 | 59 | @Parameter(names = {"-N", "--no-aspect"}, description = "Ignore aspect ratio") 60 | private boolean ignoreAspectRatio = false; 61 | 62 | @Parameter(names = {"-o", "--output"}, description = "Output filename") 63 | private String outputFilename; 64 | 65 | @Parameter(names = {"-P", "--preview"}, description = "Preview image filename") 66 | private String previewFilename; 67 | 68 | @Parameter(description = "Input file") 69 | private List files = new ArrayList<>(); 70 | 71 | @Parameter(names = {"-h", "--help"}, description = "Print usage", help = true) 72 | private boolean help; 73 | 74 | @Parameter(names = {"--resize-mode"}, description = "Resize mode") 75 | private ResizeMode resizeMode; 76 | } 77 | 78 | public enum ResizeMode { 79 | SPEED, 80 | QUALITY_NATIVE, 81 | QUALITY 82 | }; 83 | 84 | public static Colorspace COLORSPACE = null; 85 | public static Platform PLATFORM = null; 86 | public static int OPTIMIZATION_LEVEL = 1; 87 | private static final Map DITHER_ARRAYS = new HashMap<>(); 88 | private static final Map PLATFORMS = new HashMap<>(); 89 | private static final Map COLORSPACES = new HashMap<>(); 90 | 91 | static { 92 | PLATFORMS.put("cc", new PlatformComputerCraft(false)); 93 | PLATFORMS.put("cc-paletted", new PlatformComputerCraft(true)); 94 | // PLATFORMS.put("oc-tier1", new PlatformOpenComputers(1)); 95 | PLATFORMS.put("oc-tier2", new PlatformOpenComputers(2)); 96 | PLATFORMS.put("oc-tier3", new PlatformOpenComputers(3)); 97 | PLATFORMS.put("zxspectrum", new PlatformZXSpectrum(0)); 98 | PLATFORMS.put("zxspectrum-dark", new PlatformZXSpectrum(1)); 99 | 100 | COLORSPACES.put("rgb", Colorspace.RGB); 101 | COLORSPACES.put("yuv", Colorspace.YUV); 102 | COLORSPACES.put("yiq", Colorspace.YIQ); 103 | 104 | DITHER_ARRAYS.put("floyd-steinberg", new float[] { 105 | 0, 0, 0, 106 | 0, 0, 7f/16f, 107 | 3f/16f, 5f/16f, 1f/16f 108 | }); 109 | DITHER_ARRAYS.put("sierra-lite", new float[] { 110 | 0, 0, 0, 111 | 0, 0, 0.5f, 112 | 0.25f, 0.25f, 0 113 | }); 114 | DITHER_ARRAYS.put("checks", new float[] { 115 | 0, 1, 116 | 1, 0, 117 | 2 118 | }); 119 | DITHER_ARRAYS.put("2x2", new float[] { 120 | 0, 2, 121 | 3, 1, 122 | 4 123 | }); 124 | DITHER_ARRAYS.put("3x3", new float[] { 125 | 0, 7, 3, 126 | 6, 5, 2, 127 | 4, 1, 8, 128 | 9 129 | }); 130 | DITHER_ARRAYS.put("4x4", new float[] { 131 | 0, 8, 2, 10, 132 | 12, 4, 14, 6, 133 | 3, 11, 1, 9, 134 | 15, 7, 13, 5, 135 | 16 136 | }); 137 | DITHER_ARRAYS.put("8x8", new float[] { 138 | 0, 48, 12, 60, 3, 51, 15, 63, 139 | 32, 16, 44, 28, 35, 19, 47, 31, 140 | 8, 56, 4, 52, 11, 59, 7, 55, 141 | 40, 24, 36, 20, 43, 27, 39, 23, 142 | 2, 50, 14, 62, 1, 49, 13, 61, 143 | 34, 18, 46, 30, 33, 17, 45, 29, 144 | 10, 58, 6, 54, 9, 57, 5, 53, 145 | 42, 26, 38, 22, 41, 25, 37, 21, 146 | 64 147 | }); 148 | 149 | for (int i = 3; i <= 8; i++) { 150 | float[] arrL = new float[i * i + 1]; 151 | float[] arrR = new float[i * i + 1]; 152 | float[] arrS = new float[i * i + 1]; 153 | arrL[i * i] = arrR[i * i] = i; 154 | arrS[i * i] = i * i; 155 | for (int j = 0; j < i; j++) { 156 | for (int k = 0; k < i; k++) { 157 | arrL[k * i + j] = ((i - 1 - j) + (i - k)) % i; 158 | arrR[k * i + j] = (j + (i - k)) % i; 159 | arrS[k * i + j] = Math.max(k, j) * Math.max(k, j); 160 | } 161 | } 162 | 163 | DITHER_ARRAYS.put("diag-l-" + i + "x" + i, arrL); 164 | DITHER_ARRAYS.put("diag-r-" + i + "x" + i, arrR); 165 | DITHER_ARRAYS.put("square-" + i + "x" + i, arrS); 166 | } 167 | 168 | for (int i = 3; i <= 8; i += 2) { 169 | float[] arrD = new float[i * i + 1]; 170 | arrD[i * i] = i * i; 171 | int center = i / 2; 172 | for (int j = 0; j < i; j++) { 173 | for (int k = 0; k < i; k++) { 174 | arrD[k * i + j] = Math.abs(j - center) + Math.abs(k - center); 175 | arrD[k * i + j] *= arrD[k * i + j]; 176 | } 177 | } 178 | DITHER_ARRAYS.put("diamond-" + i + "x" + i, arrD); 179 | } 180 | 181 | DITHER_ARRAYS.put("diagl-4x4", new float[] { 182 | 3, 2, 1, 0, 183 | 2, 1, 0, 3, 184 | 1, 0, 3, 2, 185 | 0, 3, 2, 1, 186 | 4 187 | }); 188 | DITHER_ARRAYS.put("diagr-4x4", new float[] { 189 | 0, 1, 2, 3, 190 | 3, 0, 1, 2, 191 | 2, 3, 0, 1, 192 | 1, 2, 3, 0, 193 | 4 194 | }); 195 | } 196 | 197 | public static boolean DEBUG = false; 198 | 199 | private static Parameters params; 200 | 201 | private static int rCeil(int x, int y) { 202 | if (x % y > 0) { 203 | return x - (x % y) + y; 204 | } else { 205 | return x; 206 | } 207 | } 208 | 209 | public static void main(String[] args) { 210 | params = new Parameters(); 211 | JCommander jCommander = new JCommander(params, args); 212 | 213 | if (params.help) { 214 | jCommander.usage(); 215 | System.exit(0); 216 | } 217 | 218 | PLATFORM = PLATFORMS.get(params.mode.toLowerCase()); 219 | COLORSPACE = COLORSPACES.get(params.colorspace.toLowerCase()); 220 | 221 | if (params.files.size() == 0) { 222 | System.err.println("No input file specified!"); 223 | System.exit(1); 224 | } 225 | 226 | if (PLATFORM == null) { 227 | System.err.println(String.format("Invalid mode: %s", params.mode)); 228 | System.exit(1); 229 | } 230 | 231 | OPTIMIZATION_LEVEL = params.optimizationLevel; 232 | DEBUG = params.debug; 233 | if (params.ditherType == null) { 234 | switch (params.ditherMode) { 235 | case ORDERED: 236 | params.ditherType = "4x4"; 237 | break; 238 | default: 239 | params.ditherType = "floyd-steinberg"; 240 | break; 241 | } 242 | } 243 | 244 | BufferedImage image = Utils.loadImage(params.files.get(0)); 245 | if (image == null) { 246 | System.err.println(String.format("Could not load image: %s", params.files.get(0))); 247 | System.exit(1); 248 | } 249 | 250 | Color[] palette = PLATFORM.getPalette(); 251 | params.w = (params.w > 0) ? rCeil(params.w, PLATFORM.getCharWidth()) : 0; 252 | params.h = (params.h > 0) ? rCeil(params.h, PLATFORM.getCharHeight()) : 0; 253 | 254 | if (params.w == 0) params.w = PLATFORM.getWidthPx(); 255 | if (params.h == 0) params.h = PLATFORM.getHeightPx(); 256 | 257 | if (!params.ignoreAspectRatio) { 258 | float x = (params.ignoreAspectRatio ? PLATFORM.getDefaultAspectRatio() : (float) image.getWidth() / image.getHeight()); 259 | float y = 1.0f; 260 | float a = Math.min(Math.min( 261 | (float) params.w / x, 262 | (float) params.h / y), 263 | (float) Math.sqrt((float) PLATFORM.getCharsPx() / (x * y))); 264 | params.w = rCeil((int) Math.floor(x * a), PLATFORM.getCharWidth()); 265 | params.h = rCeil((int) Math.floor(y * a), PLATFORM.getCharHeight()); 266 | } 267 | 268 | System.out.println(params.w + " " + params.h); 269 | 270 | if (params.w * params.h > PLATFORM.getCharsPx()) { 271 | System.err.println(String.format("Size too large: %dx%d (maximum size: %d pixels)", params.w, params.h, PLATFORM.getCharsPx())); 272 | System.exit(1); 273 | } else if (params.w > PLATFORM.getWidthPx()) { 274 | System.err.println(String.format("Width too large: %d (maximum width: %d)", params.w, PLATFORM.getWidthPx())); 275 | System.exit(1); 276 | } else if (params.h > PLATFORM.getHeightPx()) { 277 | System.err.println(String.format("Height too large: %d (maximum height: %d)", params.h, PLATFORM.getHeightPx())); 278 | System.exit(1); 279 | } 280 | 281 | int width = params.w; 282 | int height = params.h; 283 | 284 | if (Main.DEBUG) { 285 | System.err.println("Using " + params.threads + " threads."); 286 | } 287 | 288 | System.err.println("Resizing image..."); 289 | long timeR = System.currentTimeMillis(); 290 | 291 | BufferedImage resizedImage; 292 | if (image.getWidth() == width && image.getHeight() == height) { 293 | resizedImage = image; 294 | } else if (params.resizeMode == ResizeMode.SPEED) { 295 | resizedImage = Utils.resizeBox(image, width, height); 296 | } else { 297 | resizedImage = Utils.resize(image, width, height, params.resizeMode == ResizeMode.QUALITY_NATIVE); 298 | } 299 | 300 | timeR = System.currentTimeMillis() - timeR; 301 | if (DEBUG) { 302 | System.err.println("Image resize time: " + timeR + " ms"); 303 | } 304 | 305 | if (PLATFORM.getCustomColorCount() > 0) { 306 | if (params.palette != null) { 307 | System.err.println("Reading palette..."); 308 | try { 309 | FileInputStream inputStream = new FileInputStream(new File(params.palette)); 310 | for (int i = 0; i < PLATFORM.getCustomColorCount(); i++) { 311 | int red = inputStream.read(); 312 | int green = inputStream.read(); 313 | int blue = inputStream.read(); 314 | palette[i] = new Color(red, green, blue); 315 | } 316 | inputStream.close(); 317 | } catch (Exception e) { 318 | throw new RuntimeException(e); 319 | } 320 | } else { 321 | long time = System.currentTimeMillis(); 322 | System.err.println("Generating palette..."); 323 | PaletteGeneratorKMeans generator = new PaletteGeneratorKMeans(resizedImage, palette, PLATFORM.getCustomColorCount(), params.paletteSamplingResolution); 324 | palette = generator.generate(params.threads); 325 | time = System.currentTimeMillis() - time; 326 | if (DEBUG) { 327 | System.err.println("Palette generation time: " + time + " ms"); 328 | } 329 | } 330 | 331 | if (params.paletteExport != null) { 332 | System.err.println("Saving palette..."); 333 | try { 334 | FileOutputStream outputStream = new FileOutputStream(new File(params.paletteExport)); 335 | for (int i = 0; i < palette.length; i++) { 336 | outputStream.write(palette[i].getRed()); 337 | outputStream.write(palette[i].getGreen()); 338 | outputStream.write(palette[i].getBlue()); 339 | } 340 | outputStream.close(); 341 | } catch (Exception e) { 342 | e.printStackTrace(); 343 | } 344 | } 345 | } 346 | 347 | try { 348 | BufferedImage outputImage = resizedImage; 349 | float[] ditherArray = DITHER_ARRAYS.get(params.ditherType.toLowerCase()); 350 | 351 | if (params.ditherLevel == 0) { 352 | params.ditherMode = Converter.DitherMode.NONE; 353 | } else if (params.ditherLevel != 1) { 354 | ditherArray = Arrays.copyOf(ditherArray, ditherArray.length); 355 | 356 | switch (params.ditherMode) { 357 | case ERROR: 358 | for (int i = 0; i < ditherArray.length; i++) { 359 | ditherArray[i] *= params.ditherLevel; 360 | } 361 | break; 362 | case ORDERED: 363 | float newScale = params.ditherLevel; 364 | float newOffset = ditherArray.length * ((1 - params.ditherLevel) / 2.0f); 365 | for (int i = 0; i < ditherArray.length; i++) { 366 | if (ditherArray[i] > 0) { 367 | ditherArray[i] = (ditherArray[i] - 1) * newScale + newOffset; 368 | } 369 | } 370 | break; 371 | } 372 | } 373 | 374 | System.err.println("Converting image..."); 375 | 376 | long time = System.currentTimeMillis(); 377 | Converter writer = new Converter(palette, resizedImage, 378 | params.ditherMode, 379 | ditherArray 380 | ); 381 | 382 | try { 383 | outputImage = writer.write(new FileOutputStream(params.outputFilename != null ? params.outputFilename : params.files.get(0) + ".ctif")); 384 | } catch (Exception e) { 385 | e.printStackTrace(); 386 | } 387 | time = System.currentTimeMillis() - time; 388 | if (DEBUG) { 389 | System.err.println("Image conversion time: " + time + " ms"); 390 | } 391 | 392 | if (params.previewFilename != null) { 393 | Utils.saveImage(Utils.resizeBox(outputImage, width * 2, height * 2), new File(params.previewFilename).getAbsolutePath()); 394 | } 395 | } catch (Exception e) { 396 | e.printStackTrace(); 397 | } 398 | } 399 | } -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/PaletteGeneratorKMeans.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif; 2 | 3 | import java.awt.Color; 4 | import java.awt.image.BufferedImage; 5 | import java.util.*; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.Executors; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | public class PaletteGeneratorKMeans { 11 | static class Result { 12 | final Color[] colors; 13 | final double error; 14 | 15 | public Result(Color[] colors, double error) { 16 | this.colors = colors; 17 | this.error = error; 18 | } 19 | } 20 | 21 | public class Worker implements Runnable { 22 | public Result result; 23 | 24 | @Override 25 | public void run() { 26 | result = generateKMeans(); 27 | } 28 | } 29 | 30 | private final int colors; 31 | private final BufferedImage image; 32 | private final Color[] base; 33 | private final Random random = new Random(); 34 | private final Map pointsAdded = new HashMap<>(); 35 | private final Map pointsWeight = new HashMap<>(); 36 | private final float[][] centroids; 37 | private final Map knownBestError = new HashMap<>(); 38 | private final Map knownBestCentroid = new HashMap<>(); 39 | 40 | public PaletteGeneratorKMeans(BufferedImage image, Color[] base, int colors, int samplingRes) { 41 | this.colors = colors; 42 | this.image = image; 43 | this.base = base; 44 | this.centroids = new float[base.length][]; 45 | 46 | if (samplingRes > 0) { 47 | int maximum = samplingRes; 48 | float stepX = (float) image.getWidth() / maximum; 49 | float stepY = (float) image.getHeight() / maximum; 50 | int stepIX = (int) Math.ceil(stepX); 51 | int stepIY = (int) Math.ceil(stepY); 52 | for (int jy = 0; jy < maximum; jy++) { 53 | for (int jx = 0; jx < maximum * 2; jx++) { 54 | int i = image.getRGB(random.nextInt(stepIX) + (int) ((jx % maximum) * stepX), random.nextInt(stepIY) + (int) (jy * stepY)); 55 | if (!pointsAdded.containsKey(i)) { 56 | float[] key = Main.COLORSPACE.fromRGB(i); 57 | pointsAdded.put(i, key); 58 | pointsWeight.put(key, 1); 59 | } else { 60 | pointsWeight.put(pointsAdded.get(i), pointsWeight.get(pointsAdded.get(i)) + 1); 61 | } 62 | } 63 | } 64 | } else { 65 | if (Main.OPTIMIZATION_LEVEL >= 3 && (image.getWidth() * image.getHeight() >= 4096)) { 66 | for (int jy = 0; jy < image.getHeight(); jy += 4) { 67 | int my = Math.min(4, image.getHeight() - jy); 68 | for (int jx = 0; jx < image.getWidth(); jx += 4) { 69 | int mx = Math.min(4, image.getWidth() - jx); 70 | 71 | int i = image.getRGB(random.nextInt(mx) + jx, random.nextInt(my) + jy); 72 | if (!pointsAdded.containsKey(i)) { 73 | float[] key = Main.COLORSPACE.fromRGB(i); 74 | pointsAdded.put(i, key); 75 | pointsWeight.put(key, 1); 76 | } else { 77 | pointsWeight.put(pointsAdded.get(i), pointsWeight.get(pointsAdded.get(i)) + 1); 78 | } 79 | } 80 | } 81 | } else { 82 | for (int i : Utils.getRGB(image)) { 83 | if (!pointsAdded.containsKey(i)) { 84 | float[] key = Main.COLORSPACE.fromRGB(i); 85 | pointsAdded.put(i, key); 86 | pointsWeight.put(key, 1); 87 | } else { 88 | pointsWeight.put(pointsAdded.get(i), pointsWeight.get(pointsAdded.get(i)) + 1); 89 | } 90 | } 91 | } 92 | } 93 | 94 | for (int i = colors; i < centroids.length; i++) { 95 | centroids[i] = Main.COLORSPACE.fromRGB(base[i].getRGB()); 96 | } 97 | 98 | for (Map.Entry weight : pointsWeight.entrySet()) { 99 | double bestError = Float.MAX_VALUE; 100 | int bestCentroid = 0; 101 | for (int i = colors; i < centroids.length; i++) { 102 | double err = Utils.getColorDistanceSq(weight.getKey(), centroids[i]); 103 | if (err < bestError) { 104 | bestError = err; 105 | bestCentroid = i; 106 | if (err == 0) break; 107 | } 108 | } 109 | knownBestError.put(weight.getKey(), bestError); 110 | knownBestCentroid.put(weight.getKey(), bestCentroid); 111 | } 112 | } 113 | 114 | public Color[] generate(int threads) { 115 | Result bestResult = null; 116 | Worker[] workers = new Worker[20 / (Main.OPTIMIZATION_LEVEL + 1)]; 117 | ExecutorService executorService = Executors.newFixedThreadPool(threads); 118 | 119 | for (int i = 0; i < workers.length; i++) { 120 | workers[i] = new Worker(); 121 | executorService.submit(workers[i]); 122 | } 123 | 124 | executorService.shutdown(); 125 | try { 126 | executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); 127 | } catch (InterruptedException e) { 128 | e.printStackTrace(); 129 | } 130 | 131 | for (int i = 0; i < workers.length; i++) { 132 | Result result = workers[i].result; 133 | if (Main.DEBUG) { 134 | System.out.println("Palette generator worker #" + (i + 1) + " error = " + result.error); 135 | } 136 | if (bestResult == null || bestResult.error > result.error) { 137 | bestResult = result; 138 | } 139 | } 140 | 141 | if (Main.DEBUG) { 142 | System.out.println("Palette generator error = " + bestResult.error); 143 | } 144 | 145 | return bestResult.colors; 146 | } 147 | 148 | private Result generateKMeans() { 149 | for (int i = 0; i < colors; i++) { 150 | centroids[i] = Main.COLORSPACE.fromRGB(image.getRGB(random.nextInt(image.getWidth()), random.nextInt(image.getHeight()))); 151 | } 152 | 153 | double totalError = 0; 154 | 155 | for (int reps = 0; reps < 128; reps++) { 156 | float[][] means = new float[centroids.length][3]; 157 | int[] meanDivs = new int[centroids.length]; 158 | 159 | totalError = 0; 160 | for (Map.Entry weight : pointsWeight.entrySet()) { 161 | double bestError = knownBestError.get(weight.getKey()); 162 | int bestCentroid = knownBestCentroid.get(weight.getKey()); 163 | int mul = weight.getValue(); 164 | 165 | for (int i = 0; i < colors; i++) { 166 | double err = Utils.getColorDistanceSq(weight.getKey(), centroids[i]); 167 | if (err < bestError) { 168 | bestError = err; 169 | bestCentroid = i; 170 | if (err == 0) break; 171 | } 172 | } 173 | 174 | totalError += bestError * mul; 175 | means[bestCentroid][0] += weight.getKey()[0] * mul; 176 | means[bestCentroid][1] += weight.getKey()[1] * mul; 177 | means[bestCentroid][2] += weight.getKey()[2] * mul; 178 | meanDivs[bestCentroid] += mul; 179 | } 180 | 181 | boolean changed = false; 182 | for (int i = 0; i < colors; i++) { 183 | if (meanDivs[i] > 0) { 184 | float n0 = means[i][0] / meanDivs[i]; 185 | float n1 = means[i][1] / meanDivs[i]; 186 | float n2 = means[i][2] / meanDivs[i]; 187 | if (n0 != centroids[i][0] || n1 != centroids[i][1] || n2 != centroids[i][2]) { 188 | centroids[i][0] = n0; 189 | centroids[i][1] = n1; 190 | centroids[i][2] = n2; 191 | changed = true; 192 | } 193 | } 194 | } 195 | if (!changed) { 196 | break; 197 | } 198 | } 199 | 200 | Color[] out = Arrays.copyOf(base, base.length); 201 | for (int k = 0; k < colors; k++) { 202 | out[k] = new Color(Main.COLORSPACE.toRGB(centroids[k]) | 0xFF000000); 203 | } 204 | return new Result(out, totalError); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/Utils.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | 9 | import org.im4java.core.ConvertCmd; 10 | import org.im4java.core.IMOperation; 11 | import org.im4java.core.Stream2BufferedImage; 12 | import org.im4java.process.OutputConsumer; 13 | import pl.asie.ctif.colorspace.Colorspace; 14 | 15 | import javax.imageio.ImageIO; 16 | 17 | public final class Utils { 18 | private static int imMode = -1; 19 | 20 | private Utils() { 21 | 22 | } 23 | 24 | private static ConvertCmd createConvertCmd() { 25 | if (imMode < 0) { 26 | ConvertCmd cmd = new ConvertCmd(); 27 | imMode = 1; 28 | 29 | try { 30 | cmd.setOutputConsumer(new OutputConsumer() { 31 | @Override 32 | public void consumeOutput(InputStream inputStream) throws IOException { 33 | 34 | } 35 | }); 36 | cmd.run(new IMOperation()); 37 | } catch (Exception e) { 38 | if (e.getCause() instanceof IOException) { 39 | System.err.println("Warning: ImageMagick not found! Please install ImageMagick for improved scaling quality."); 40 | imMode = 0; 41 | } 42 | } 43 | 44 | if (imMode == 1) { 45 | System.err.println("ImageMagick found; using ImageMagick for resizing."); 46 | } 47 | } 48 | 49 | switch (imMode) { 50 | case 0: 51 | default: 52 | return null; 53 | case 1: 54 | return new ConvertCmd(); 55 | case 2: 56 | return new ConvertCmd(true); 57 | } 58 | } 59 | 60 | public static BufferedImage resize(BufferedImage image, int width, int height, boolean forceNoImagemagick) { 61 | ConvertCmd cmd = forceNoImagemagick ? null : createConvertCmd(); 62 | if (cmd == null) { 63 | BufferedImage resizedImage = new BufferedImage(width, height, image.getType()); 64 | Graphics2D g = resizedImage.createGraphics(); 65 | g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); 66 | g.drawImage(image, 0, 0, width, height, null); 67 | g.dispose(); 68 | return resizedImage; 69 | } 70 | IMOperation op = new IMOperation(); 71 | op.addImage(); 72 | op.colorspace("RGB"); 73 | op.filter("LanczosRadius"); 74 | op.resize(width, height, '!'); 75 | op.colorspace("sRGB"); 76 | op.addImage("png:-"); 77 | 78 | Stream2BufferedImage s2b = new Stream2BufferedImage(); 79 | cmd.setOutputConsumer(s2b); 80 | try { 81 | cmd.run(op, image); 82 | } catch (Exception e) { 83 | e.printStackTrace(); 84 | } 85 | return s2b.getImage(); 86 | } 87 | 88 | public static BufferedImage resizeBox(BufferedImage image, int width, int height) { 89 | BufferedImage resizedImage = new BufferedImage(width, height, image.getType()); 90 | Graphics2D g = resizedImage.createGraphics(); 91 | g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); 92 | g.drawImage(image, 0, 0, width, height, null); 93 | g.dispose(); 94 | return resizedImage; 95 | } 96 | 97 | public static void saveImage(BufferedImage image, String location) { 98 | try { 99 | ImageIO.write(image, "png", new File(location)); 100 | } catch (Exception e) { 101 | e.printStackTrace(); 102 | } 103 | } 104 | 105 | public static BufferedImage loadImage(String location) { 106 | try { 107 | if (location.equals("-")) { 108 | return ImageIO.read(System.in); 109 | } else { 110 | return ImageIO.read(new File(location)); 111 | } 112 | } catch (Exception e) { 113 | e.printStackTrace(); 114 | return null; 115 | } 116 | } 117 | 118 | public static int[] getRGB(BufferedImage image) { 119 | return image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth()); 120 | } 121 | 122 | public static void addQuantError(float[] target, float[] expected, float[] received, float mul) { 123 | if (mul != 0.0f) { 124 | for (int i = 0; i < target.length; i++) { 125 | target[i] += (expected[i] - received[i]) * mul; 126 | } 127 | } 128 | } 129 | 130 | public static double getColorDistance(int c1, int c2, Colorspace colorspace) { 131 | return Math.sqrt(getColorDistanceSq(c1, c2, colorspace)); 132 | } 133 | 134 | public static double getColorDistanceSq(int c1, int c2, Colorspace colorspace) { 135 | return getColorDistanceSq(colorspace.fromRGB(c1), colorspace.fromRGB(c2)); 136 | } 137 | 138 | public static double getColorDistanceSq(float[] f1, float[] f2) { 139 | return (f1[0] - f2[0]) * (f1[0] - f2[0]) + 140 | (f1[1] - f2[1]) * (f1[1] - f2[1]) + 141 | (f1[2] - f2[2]) * (f1[2] - f2[2]); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/colorspace/Colorspace.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif.colorspace; 2 | 3 | public abstract class Colorspace { 4 | public static final Colorspace YIQ; 5 | public static final Colorspace YUV; 6 | public static final Colorspace RGB; 7 | 8 | static { 9 | YUV = new ColorspaceMatrix( 10 | new float[]{ 11 | 0.299f, 0.587f, 0.114f, 12 | -0.147f, -0.289f, 0.436f, 13 | 0.615f, -0.515f, -0.100f 14 | } 15 | ); 16 | YIQ = new ColorspaceMatrix( 17 | new float[]{ 18 | 0.299f, 0.587f, 0.114f, 19 | 0.595716f, -0.274453f, -0.321263f, 20 | 0.211456f, -0.522591f, 0.311135f 21 | } 22 | ); 23 | RGB = new Colorspace() { 24 | @Override 25 | public float[] fromRGB(float[] value) { 26 | return value; 27 | } 28 | 29 | @Override 30 | public float[] toRGBArray(float[] value) { 31 | return value; 32 | } 33 | }; 34 | } 35 | 36 | public int toRGB(float[] value) { 37 | float[] rgb = toRGBArray(value); 38 | if (rgb[0] < 0) rgb[0] = 0; else if (rgb[0] > 1) rgb[0] = 1; 39 | if (rgb[1] < 0) rgb[1] = 0; else if (rgb[1] > 1) rgb[1] = 1; 40 | if (rgb[2] < 0) rgb[2] = 0; else if (rgb[2] > 1) rgb[2] = 1; 41 | return (Math.round(rgb[0] * 255.0f) << 16) | (Math.round(rgb[1] * 255.0f) << 8) | Math.round(rgb[2] * 255.0f); 42 | } 43 | 44 | public float[] fromRGB(int value) { 45 | return fromRGB(new float[] { 46 | (float) ((value >> 16) & 0xFF) / 255.0f, 47 | (float) ((value >> 8) & 0xFF) / 255.0f, 48 | (float) (value & 0xFF) / 255.0f 49 | }); 50 | } 51 | 52 | public abstract float[] fromRGB(float[] value); 53 | 54 | public abstract float[] toRGBArray(float[] value); 55 | } 56 | -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/colorspace/ColorspaceMatrix.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif.colorspace; 2 | 3 | import java.util.Arrays; 4 | 5 | /** 6 | * Created by asie on 5/9/17. 7 | */ 8 | public class ColorspaceMatrix extends Colorspace { 9 | private final Colorspace parent; 10 | private final float[] matrixFromRGB; 11 | private final float[] matrixToRGB; 12 | 13 | protected ColorspaceMatrix(float[] matrix) { 14 | this(null, matrix); 15 | } 16 | 17 | protected ColorspaceMatrix(Colorspace parent, float[] matrix) { 18 | this.parent = null; 19 | this.matrixFromRGB = matrix; 20 | this.matrixToRGB = new float[9]; 21 | float detInv = 1f / (matrix[0]*matrix[4]*matrix[8] + matrix[1]*matrix[5]*matrix[6] + matrix[2]*matrix[3]*matrix[7] 22 | - (matrix[2]*matrix[4]*matrix[6] + matrix[0]*matrix[5]*matrix[7] + matrix[1]*matrix[3]*matrix[8])); 23 | matrixToRGB[0] = detInv*(matrix[1*3+1]*matrix[2*3+2] - matrix[1*3+2]*matrix[2*3+1]); 24 | matrixToRGB[3] = -detInv*(matrix[1*3+0]*matrix[2*3+2] - matrix[1*3+2]*matrix[2*3+0]); 25 | matrixToRGB[6] = -detInv*(matrix[1*3+1]*matrix[2*3+0] - matrix[1*3+0]*matrix[2*3+1]); 26 | matrixToRGB[1] = -detInv*(matrix[0*3+1]*matrix[2*3+2] - matrix[0*3+2]*matrix[2*3+1]); 27 | matrixToRGB[4] = detInv*(matrix[0*3+0]*matrix[2*3+2] - matrix[0*3+2]*matrix[2*3+0]); 28 | matrixToRGB[7] = detInv*(matrix[0*3+1]*matrix[2*3+0] - matrix[0*3+0]*matrix[2*3+1]); 29 | matrixToRGB[2] = detInv*(matrix[0*3+1]*matrix[1*3+2] - matrix[0*3+2]*matrix[1*3+1]); 30 | matrixToRGB[5] = -detInv*(matrix[0*3+0]*matrix[1*3+2] - matrix[0*3+2]*matrix[1*3+0]); 31 | matrixToRGB[8] = -detInv*(matrix[0*3+1]*matrix[1*3+0] - matrix[0*3+0]*matrix[1*3+1]); 32 | } 33 | 34 | protected final float[] mmul3(float[] a, float[] b) { 35 | return new float[] { 36 | a[0] * b[0] + a[1] * b[1] + a[2] * b[2], 37 | a[0] * b[3] + a[1] * b[4] + a[2] * b[5], 38 | a[0] * b[6] + a[1] * b[7] + a[2] * b[8] 39 | }; 40 | } 41 | 42 | @Override 43 | public float[] fromRGB(float[] value) { 44 | return mmul3(parent != null ? parent.fromRGB(value) : value, matrixFromRGB); 45 | } 46 | 47 | @Override 48 | public float[] toRGBArray(float[] value) { 49 | return parent != null ? parent.toRGBArray(mmul3(value, matrixToRGB)) : mmul3(value, matrixToRGB); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/platform/Platform.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif.platform; 2 | 3 | import java.awt.*; 4 | 5 | public abstract class Platform { 6 | public final int platformId; 7 | private final int charWidth, charHeight, width, height, customColorCount; 8 | private Color[] palette = null; 9 | 10 | protected Platform(int platformId, int charWidth, int charHeight, int width, int height, 11 | int customColorCount) { 12 | this.platformId = platformId; 13 | this.charWidth = charWidth; 14 | this.charHeight = charHeight; 15 | this.width = width; 16 | this.height = height; 17 | this.customColorCount = customColorCount; 18 | } 19 | 20 | abstract Color[] generatePalette(); 21 | 22 | public int getCharWidth() { 23 | return charWidth; 24 | } 25 | 26 | public int getCharHeight() { 27 | return charHeight; 28 | } 29 | 30 | public int getWidth() { 31 | return width; 32 | } 33 | 34 | public int getHeight() { 35 | return height; 36 | } 37 | 38 | public int getChars() { 39 | return getWidth() * getHeight(); 40 | } 41 | 42 | public int getCustomColorCount() { 43 | return customColorCount; 44 | } 45 | 46 | public Color[] getPalette() { 47 | if (palette == null) { 48 | palette = generatePalette(); 49 | } 50 | return palette.clone(); 51 | } 52 | 53 | public float getDefaultAspectRatio() { 54 | return (float) getWidth() / getHeight(); 55 | } 56 | 57 | public final int getWidthPx() { 58 | return getWidth() * getCharWidth(); 59 | } 60 | 61 | public final int getHeightPx() { 62 | return getHeight() * getCharHeight(); 63 | } 64 | 65 | public final int getCharsPx() { 66 | return getChars() * getCharWidth() * getCharHeight(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/platform/PlatformComputerCraft.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif.platform; 2 | 3 | import java.awt.*; 4 | 5 | public class PlatformComputerCraft extends Platform { 6 | public PlatformComputerCraft(boolean customPaletteAllowed) { 7 | super(2, 2, 3, 164, 81, customPaletteAllowed ? 16 : 0); 8 | } 9 | 10 | @Override 11 | Color[] generatePalette() { 12 | Color[] colors = new Color[16]; 13 | colors[0] = new Color(0xf0f0f0); 14 | colors[1] = new Color(0xf2b233); 15 | colors[2] = new Color(0xe57fd8); 16 | colors[3] = new Color(0x99b2f2); 17 | colors[4] = new Color(0xdede6c); 18 | colors[5] = new Color(0x7fcc19); 19 | colors[6] = new Color(0xf2b2cc); 20 | colors[7] = new Color(0x4c4c4c); 21 | colors[8] = new Color(0x999999); 22 | colors[9] = new Color(0x4c99b2); 23 | colors[10] = new Color(0xb266e5); 24 | colors[11] = new Color(0x3366cc); 25 | colors[12] = new Color(0x7f664c); 26 | colors[13] = new Color(0x57a64e); 27 | colors[14] = new Color(0xcc4c4c); 28 | colors[15] = new Color(0x0c0c0c); 29 | return colors; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/platform/PlatformOpenComputers.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif.platform; 2 | 3 | import java.awt.*; 4 | 5 | public class PlatformOpenComputers extends Platform { 6 | public final int tier; 7 | 8 | public PlatformOpenComputers(int tier) { 9 | super(1, 2, 4, 0, 0, 0); 10 | this.tier = tier; 11 | } 12 | 13 | @Override 14 | public int getCustomColorCount() { 15 | switch (tier) { 16 | case 1: 17 | default: 18 | return 0; 19 | case 2: 20 | case 3: 21 | return 16; 22 | } 23 | } 24 | 25 | @Override 26 | public int getWidth() { 27 | switch (tier) { 28 | case 1: 29 | return 40; 30 | case 2: 31 | return 80; 32 | case 3: 33 | return 160; 34 | default: 35 | return 0; 36 | } 37 | } 38 | 39 | protected int getRealHeight() { 40 | switch (tier) { 41 | case 1: 42 | case 2: 43 | return 25; 44 | case 3: 45 | return 50; 46 | default: 47 | return 0; 48 | } 49 | } 50 | 51 | @Override 52 | public int getHeight() { 53 | return getWidth(); 54 | } 55 | 56 | @Override 57 | public float getDefaultAspectRatio() { 58 | return (float) getWidth() / getRealHeight(); 59 | } 60 | 61 | @Override 62 | public int getChars() { 63 | return getWidth() * getRealHeight(); 64 | } 65 | 66 | @Override 67 | Color[] generatePalette() { 68 | Color[] colors = new Color[getColorCount()]; 69 | if (tier == 1) { 70 | colors[0] = new Color(0, 0, 0); 71 | colors[1] = new Color(255, 255, 255); 72 | } else { 73 | for (int i = 0; i < 16; i++) { 74 | colors[i] = new Color(17 * i, 17 * i, 17 * i); 75 | } 76 | 77 | if (tier == 3) { 78 | for (int i = 0; i < 240; i++) { 79 | colors[i + 16] = new Color( 80 | ((i / 40) % 6) * 255 / 5, 81 | ((i / 5) % 8) * 255 / 7, 82 | (i % 5) * 255 / 4 83 | ); 84 | } 85 | } 86 | } 87 | return colors; 88 | } 89 | 90 | protected int getColorCount() { 91 | switch (tier) { 92 | case 1: 93 | return 2; 94 | case 2: 95 | default: 96 | return 16; 97 | case 3: 98 | return 256; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /converter/src/pl/asie/ctif/platform/PlatformZXSpectrum.java: -------------------------------------------------------------------------------- 1 | package pl.asie.ctif.platform; 2 | 3 | import java.awt.*; 4 | 5 | public class PlatformZXSpectrum extends Platform { 6 | private final int paletteMode; 7 | 8 | public PlatformZXSpectrum(int paletteMode) { 9 | super(3, 8, 8, 256/8, 192/8, 0); 10 | this.paletteMode = paletteMode; 11 | } 12 | 13 | @Override 14 | Color[] generatePalette() { 15 | Color[] colors = new Color[paletteMode == 2 ? 16 : 8]; 16 | colors[0] = new Color(0x000000); 17 | colors[1] = new Color(0x0000ff); 18 | colors[2] = new Color(0xff0000); 19 | colors[3] = new Color(0xff00ff); 20 | colors[4] = new Color(0x00ff00); 21 | colors[5] = new Color(0x00ffff); 22 | colors[6] = new Color(0xffff00); 23 | colors[7] = new Color(0xffffff); 24 | if (paletteMode > 0) { 25 | for (int i = 0; i < 8; i++) { 26 | Color c = colors[i]; 27 | colors[i] = new Color(c.getRGB() & 0x00d7d7d7); 28 | if (paletteMode == 2) colors[8+i] = c; 29 | } 30 | } 31 | return colors; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /viewers/ctif-cc-1.8.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ctif-cc: ComputerCraft viewer for CTIF image files 3 | Copyright (c) 2016, 2017, 2018 asie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | ]] 23 | 24 | local pal = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"} 25 | local oldCols = {} 26 | local colsChanged = false 27 | local ctable = {} 28 | local hdr = {67,84,73,70} 29 | local args = {...} 30 | 31 | -- No Lua 5.3? Oh well. 32 | for a=0,1 do 33 | for b=0,1 do 34 | for c=0,1 do 35 | for d=0,1 do 36 | for e=0,1 do 37 | for f=0,1 do 38 | ctable[1 + a*32 + b*16 + c*8 + d*4 + e*2 + f] = e*16 + d*8 + c*4 + b*2 + a 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | 46 | local file = fs.open(args[1], "rb") 47 | local t = term 48 | local isMonitor = false 49 | if #args >= 2 then 50 | t = peripheral.wrap(args[2]) 51 | isMonitor = true 52 | end 53 | 54 | function readShort() 55 | local x = file.read() 56 | return x + (file.read()) * 256 57 | end 58 | 59 | -- Verify header 60 | for i=1,4 do 61 | if file.read() ~= hdr[i] then 62 | error("Invalid header!") 63 | end 64 | end 65 | 66 | local hdrVersion = file.read() 67 | if hdrVersion > 1 then 68 | error("Unknown header version: " .. hdrVersion) 69 | end 70 | 71 | local platformVariant = file.read() 72 | local platformId = readShort() 73 | if platformId ~= 2 or platformVariant ~= 0 then 74 | error("Unsupported platform ID: " .. platformId .. ":" .. platformVariant) 75 | end 76 | 77 | local width = readShort() 78 | local height = readShort() 79 | 80 | local pw = file.read() 81 | local ph = file.read() 82 | if not (pw == 2 and ph == 3) and not (pw == 1 and ph == 1) then 83 | error("Unsupported character width: " .. pw .. "x" .. ph) 84 | end 85 | 86 | local bpp = file.read() 87 | if bpp ~= 4 then 88 | error("Unsupported bit depth: " .. bpp) 89 | end 90 | 91 | local ccEntrySize = file.read() 92 | local ccArraySize = readShort() 93 | if ccArraySize > 0 then 94 | if t.setPaletteColour == nil then 95 | error("Custom colors are not supported on this version of ComputerCraft! Please upgrade.") 96 | else 97 | for i=0,ccArraySize-1 do 98 | local oldColR, oldColG, oldColB = t.getPaletteColour(bit.blshift(1, i)) 99 | oldCols[i*3 + 1] = oldColR 100 | oldCols[i*3 + 2] = oldColG 101 | oldCols[i*3 + 3] = oldColB 102 | local colB = file.read() / 255 103 | local colG = file.read() / 255 104 | local colR = file.read() / 255 105 | t.setPaletteColour(bit.blshift(1, i), colR, colG, colB) 106 | end 107 | colsChanged = true 108 | end 109 | end 110 | 111 | -- Prepare ideal terminal size 112 | t.clear() 113 | if isMonitor then 114 | t.setTextScale(0.5) 115 | end 116 | local xoff = -1 117 | local yoff = -1 118 | local xbase, ybase = t.getSize() 119 | if isMonitor then 120 | for xdiv=5,0.5,-0.5 do 121 | local xsize = math.floor(xbase / (xdiv * 2)) 122 | local ysize = math.floor(ybase / (xdiv * 2)) 123 | if xsize >= width and ysize >= height then 124 | t.setTextScale(xdiv) 125 | xoff = math.floor((xsize - width) / 2) + 1 126 | yoff = math.floor((ysize - height) / 2) + 1 127 | break 128 | end 129 | end 130 | else 131 | if xbase >= width and ybase >= height then 132 | xoff = math.floor((xbase - width) / 2) + 1 133 | yoff = math.floor((ybase - height) / 2) + 1 134 | end 135 | end 136 | 137 | if (xoff < 0) or (yoff < 0) then 138 | error("Image too large for ComputerCraft: " .. width .. "x" .. height .. " (max: " .. xbase .. "x" .. ybase .. ")") 139 | end 140 | 141 | -- Draw 142 | for y=0,height-1 do 143 | local bgs = "" 144 | local fgs = "" 145 | local chs = "" 146 | for x=0,width-1 do 147 | if pw*ph == 1 then 148 | bgs = bgs .. pal[1 + file.read()] 149 | fgs = fgs .. "0" 150 | chs = chs .. " " 151 | else 152 | local col = file.read() 153 | local ch = file.read() 154 | local fg = col % 16 155 | local bg = (col - fg) / 16 156 | if ch % 2 == 1 then 157 | ch = 63 - ch 158 | local t = fg 159 | fg = bg 160 | bg = t 161 | end 162 | bgs = bgs .. pal[1 + bg] 163 | fgs = fgs .. pal[1 + fg] 164 | chs = chs .. string.char(128 + ctable[1 + ch]) 165 | end 166 | end 167 | t.setCursorPos(xoff, yoff + y) 168 | t.blit(chs, fgs, bgs) 169 | end 170 | 171 | file.close() 172 | 173 | local event, key = os.pullEvent( "key" ) 174 | 175 | if colsChanged then 176 | for i=0,ccArraySize-1 do 177 | t.setPaletteColour(bit.blshift(1, i), oldCols[i*3 + 1], oldCols[i*3 + 2], oldCols[i*3 + 3]) 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /viewers/ctif-cc.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ctif-cc: ComputerCraft viewer for CTIF image files 3 | Copyright (c) 2016, 2017, 2018 asie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | ]] 23 | 24 | local pal = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"} 25 | local oldCols = {} 26 | local colsChanged = false 27 | local ctable = {} 28 | local hdr = {67,84,73,70} 29 | local args = {...} 30 | 31 | -- No Lua 5.3? Oh well. 32 | for a=0,1 do 33 | for b=0,1 do 34 | for c=0,1 do 35 | for d=0,1 do 36 | for e=0,1 do 37 | for f=0,1 do 38 | ctable[1 + a*32 + b*16 + c*8 + d*4 + e*2 + f] = e*16 + d*8 + c*4 + b*2 + a 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | 46 | local file = fs.open(args[1], "rb") 47 | local t = term 48 | local isMonitor = false 49 | if #args >= 2 then 50 | t = peripheral.wrap(args[2]) 51 | isMonitor = true 52 | end 53 | 54 | function readShort() 55 | local x = file:read() 56 | return x + (file:read()) * 256 57 | end 58 | 59 | -- Verify header 60 | for i=1,4 do 61 | if file:read() ~= hdr[i] then 62 | error("Invalid header!") 63 | end 64 | end 65 | 66 | local hdrVersion = file:read() 67 | if hdrVersion > 1 then 68 | error("Unknown header version: " .. hdrVersion) 69 | end 70 | 71 | local platformVariant = file:read() 72 | local platformId = readShort() 73 | if platformId ~= 2 or platformVariant ~= 0 then 74 | error("Unsupported platform ID: " .. platformId .. ":" .. platformVariant) 75 | end 76 | 77 | local width = readShort() 78 | local height = readShort() 79 | 80 | local pw = file:read() 81 | local ph = file:read() 82 | if not (pw == 2 and ph == 3) and not (pw == 1 and ph == 1) then 83 | error("Unsupported character width: " .. pw .. "x" .. ph) 84 | end 85 | 86 | local bpp = file:read() 87 | if bpp ~= 4 then 88 | error("Unsupported bit depth: " .. bpp) 89 | end 90 | 91 | local ccEntrySize = file:read() 92 | local ccArraySize = readShort() 93 | if ccArraySize > 0 then 94 | if t.setPaletteColour == nil then 95 | error("Custom colors are not supported on this version of ComputerCraft! Please upgrade.") 96 | else 97 | for i=0,ccArraySize-1 do 98 | local oldColR, oldColG, oldColB = t.getPaletteColour(bit.blshift(1, i)) 99 | oldCols[i*3 + 1] = oldColR 100 | oldCols[i*3 + 2] = oldColG 101 | oldCols[i*3 + 3] = oldColB 102 | local colB = file:read() / 255 103 | local colG = file:read() / 255 104 | local colR = file:read() / 255 105 | t.setPaletteColour(bit.blshift(1, i), colR, colG, colB) 106 | end 107 | colsChanged = true 108 | end 109 | end 110 | 111 | -- Prepare ideal terminal size 112 | t.clear() 113 | if isMonitor then 114 | t.setTextScale(0.5) 115 | end 116 | local xoff = -1 117 | local yoff = -1 118 | local xbase, ybase = t.getSize() 119 | if isMonitor then 120 | for xdiv=5,0.5,-0.5 do 121 | local xsize = math.floor(xbase / (xdiv * 2)) 122 | local ysize = math.floor(ybase / (xdiv * 2)) 123 | if xsize >= width and ysize >= height then 124 | t.setTextScale(xdiv) 125 | xoff = math.floor((xsize - width) / 2) + 1 126 | yoff = math.floor((ysize - height) / 2) + 1 127 | break 128 | end 129 | end 130 | else 131 | if xbase >= width and ybase >= height then 132 | xoff = math.floor((xbase - width) / 2) + 1 133 | yoff = math.floor((ybase - height) / 2) + 1 134 | end 135 | end 136 | 137 | if (xoff < 0) or (yoff < 0) then 138 | error("Image too large for ComputerCraft: " .. width .. "x" .. height .. " (max: " .. xbase .. "x" .. ybase .. ")") 139 | end 140 | 141 | -- Draw 142 | for y=0,height-1 do 143 | local bgs = "" 144 | local fgs = "" 145 | local chs = "" 146 | for x=0,width-1 do 147 | if pw*ph == 1 then 148 | bgs = bgs .. pal[1 + file:read()] 149 | fgs = fgs .. "0" 150 | chs = chs .. " " 151 | else 152 | local col = file:read() 153 | local ch = file:read() 154 | local fg = col % 16 155 | local bg = (col - fg) / 16 156 | if ch % 2 == 1 then 157 | ch = 63 - ch 158 | local t = fg 159 | fg = bg 160 | bg = t 161 | end 162 | bgs = bgs .. pal[1 + bg] 163 | fgs = fgs .. pal[1 + fg] 164 | chs = chs .. string.char(128 + ctable[1 + ch]) 165 | end 166 | end 167 | t.setCursorPos(xoff, yoff + y) 168 | t.blit(chs, fgs, bgs) 169 | end 170 | 171 | local event, key = os.pullEvent( "key" ) 172 | 173 | if colsChanged then 174 | for i=0,ccArraySize-1 do 175 | t.setPaletteColour(bit.blshift(1, i), oldCols[i*3 + 1], oldCols[i*3 + 2], oldCols[i*3 + 3]) 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /viewers/oc/ctifview.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ctif-oc: OpenComputers viewer for CTIF image files 3 | Copyright (c) 2016, 2017, 2018 asie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | ]] 23 | 24 | local args = {...} 25 | local component = require("component") 26 | local event = require("event") 27 | local gpu = component.gpu 28 | local unicode = require("unicode") 29 | local keyboard = require("keyboard") 30 | local text = require("text") 31 | local os = require("os") 32 | local pal = {} 33 | 34 | local q = {} 35 | for i=0,255 do 36 | local dat = (i & 0x01) << 7 37 | dat = dat | (i & 0x02) >> 1 << 6 38 | dat = dat | (i & 0x04) >> 2 << 5 39 | dat = dat | (i & 0x08) >> 3 << 2 40 | dat = dat | (i & 0x10) >> 4 << 4 41 | dat = dat | (i & 0x20) >> 5 << 1 42 | dat = dat | (i & 0x40) >> 6 << 3 43 | dat = dat | (i & 0x80) >> 7 44 | q[i + 1] = unicode.char(0x2800 | dat) 45 | end 46 | 47 | function error(str) 48 | print("ERROR: " .. str) 49 | os.exit() 50 | end 51 | 52 | function resetPalette(data) 53 | for i=0,255 do 54 | if (i < 16) then 55 | if data == nil or data[3] == nil or data[3][i] == nil then 56 | pal[i] = (i * 15) << 16 | (i * 15) << 8 | (i * 15) 57 | else 58 | pal[i] = data[3][i] 59 | gpu.setPaletteColor(i, data[3][i]) 60 | end 61 | else 62 | local j = i - 16 63 | local b = math.floor((j % 5) * 255 / 4.0) 64 | local g = math.floor((math.floor(j / 5.0) % 8) * 255 / 7.0) 65 | local r = math.floor((math.floor(j / 40.0) % 6) * 255 / 5.0) 66 | pal[i] = r << 16 | g << 8 | b 67 | end 68 | end 69 | end 70 | 71 | resetPalette(nil) 72 | 73 | function r8(file) 74 | local byte = file:read(1) 75 | if byte == nil then 76 | return 0 77 | else 78 | return string.byte(byte) & 255 79 | end 80 | end 81 | 82 | function r16(file) 83 | local x = r8(file) 84 | return x | (r8(file) << 8) 85 | end 86 | 87 | function loadImage(filename) 88 | local data = {} 89 | local file = io.open(filename, 'rb') 90 | local hdr = {67,84,73,70} 91 | 92 | for i=1,4 do 93 | if r8(file) ~= hdr[i] then 94 | error("Invalid header!") 95 | end 96 | end 97 | 98 | local hdrVersion = r8(file) 99 | local platformVariant = r8(file) 100 | local platformId = r16(file) 101 | 102 | if hdrVersion > 1 then 103 | error("Unknown header version: " .. hdrVersion) 104 | end 105 | 106 | if platformId ~= 1 or platformVariant ~= 0 then 107 | error("Unsupported platform ID: " .. platformId .. ":" .. platformVariant) 108 | end 109 | 110 | data[1] = {} 111 | data[2] = {} 112 | data[3] = {} 113 | data[2][1] = r8(file) 114 | data[2][1] = (data[2][1] | (r8(file) << 8)) 115 | data[2][2] = r8(file) 116 | data[2][2] = (data[2][2] | (r8(file) << 8)) 117 | 118 | local pw = r8(file) 119 | local ph = r8(file) 120 | if not (pw == 2 and ph == 4) then 121 | error("Unsupported character width: " .. pw .. "x" .. ph) 122 | end 123 | 124 | data[2][3] = r8(file) 125 | if (data[2][3] ~= 4 and data[2][3] ~= 8) or data[2][3] > gpu.getDepth() then 126 | error("Unsupported bit depth: " .. data[2][3]) 127 | end 128 | 129 | local ccEntrySize = r8(file) 130 | local customColors = r16(file) 131 | if customColors > 0 and ccEntrySize ~= 3 then 132 | error("Unsupported palette entry size: " .. ccEntrySize) 133 | end 134 | if customColors > 16 then 135 | error("Unsupported palette entry amount: " .. customColors) 136 | end 137 | 138 | for p=0,customColors-1 do 139 | local w = r16(file) 140 | data[3][p] = w | (r8(file) << 16) 141 | end 142 | 143 | local WIDTH = data[2][1] 144 | local HEIGHT = data[2][2] 145 | 146 | for y=0,HEIGHT-1 do 147 | for x=0,WIDTH-1 do 148 | local j = (y * WIDTH) + x + 1 149 | local w = r16(file) 150 | if data[2][3] > 4 then 151 | data[1][j] = w | (r8(file) << 16) 152 | else 153 | data[1][j] = w 154 | end 155 | end 156 | end 157 | 158 | io.close(file) 159 | return data 160 | end 161 | 162 | function gpuBG() 163 | local a, al = gpu.getBackground() 164 | if al then 165 | return gpu.getPaletteColor(a) 166 | else 167 | return a 168 | end 169 | end 170 | function gpuFG() 171 | local a, al = gpu.getForeground() 172 | if al then 173 | return gpu.getPaletteColor(a) 174 | else 175 | return a 176 | end 177 | end 178 | 179 | function drawImage(data, offx, offy) 180 | if offx == nil then offx = 0 end 181 | if offy == nil then offy = 0 end 182 | 183 | local WIDTH = data[2][1] 184 | local HEIGHT = data[2][2] 185 | 186 | gpu.setResolution(WIDTH, HEIGHT) 187 | resetPalette(data) 188 | 189 | local bg = 0 190 | local fg = 0 191 | local cw = 1 192 | local noBG = false 193 | local noFG = false 194 | local ind = 1 195 | 196 | local gBG = gpuBG() 197 | local gFG = gpuFG() 198 | 199 | for y=0,HEIGHT-1 do 200 | local str = "" 201 | for x=0,WIDTH-1 do 202 | ind = (y * WIDTH) + x + 1 203 | if data[2][3] > 4 then 204 | bg = pal[data[1][ind] & 0xFF] 205 | fg = pal[(data[1][ind] >> 8) & 0xFF] 206 | cw = ((data[1][ind] >> 16) & 0xFF) + 1 207 | else 208 | fg = pal[data[1][ind] & 0x0F] 209 | bg = pal[(data[1][ind] >> 4) & 0x0F] 210 | cw = ((data[1][ind] >> 8) & 0xFF) + 1 211 | end 212 | noBG = (cw == 256) 213 | noFG = (cw == 1) 214 | if (noFG or (gBG == fg)) and (noBG or (gFG == bg)) then 215 | str = str .. q[257 - cw] 216 | -- str = str .. "I" 217 | elseif (noBG or (gBG == bg)) and (noFG or (gFG == fg)) then 218 | str = str .. q[cw] 219 | else 220 | if #str > 0 then 221 | gpu.set(x + 1 + offx - unicode.wlen(str), y + 1 + offy, str) 222 | end 223 | if (gBG == fg and gFG ~= bg) or (gFG == bg and gBG ~= fg) then 224 | cw = 257 - cw 225 | local t = bg 226 | bg = fg 227 | fg = t 228 | end 229 | if gBG ~= bg then 230 | gpu.setBackground(bg) 231 | gBG = bg 232 | end 233 | if gFG ~= fg then 234 | gpu.setForeground(fg) 235 | gFG = fg 236 | end 237 | str = q[cw] 238 | -- if (not noBG) and (not noFG) then str = "C" elseif (not noBG) then str = "B" elseif (not noFG) then str = "F" else str = "c" end 239 | end 240 | end 241 | if #str > 0 then 242 | gpu.set(WIDTH + 1 - unicode.wlen(str) + offx, y + 1 + offy, str) 243 | end 244 | end 245 | end 246 | 247 | local image = loadImage(args[1]) 248 | drawImage(image) 249 | 250 | while true do 251 | local name,addr,char,key,player = event.pull("key_down") 252 | if key == 0x10 then 253 | break 254 | end 255 | end 256 | 257 | gpu.setBackground(0, false) 258 | gpu.setForeground(16777215, false) 259 | gpu.setResolution(80, 25) 260 | gpu.fill(1, 1, 80, 25, " ") 261 | --------------------------------------------------------------------------------