diff --git a/src/org/nschmidt/ldparteditor/composite/compositetab/CompositeTab.java b/src/org/nschmidt/ldparteditor/composite/compositetab/CompositeTab.java index 93c32e096..3db1d0226 100644 --- a/src/org/nschmidt/ldparteditor/composite/compositetab/CompositeTab.java +++ b/src/org/nschmidt/ldparteditor/composite/compositetab/CompositeTab.java @@ -1854,7 +1854,7 @@ public void mouseDown(MouseEvent e) { int toLine = s2 > -1 ? st.getLineAtOffset(s2) : s2 * -1; fromLine++; toLine++; - DataMetacommandExporter.export(fromLine, toLine, df); + DataMetacommandExporter.export(fromLine, toLine, df, tabState.window[0].getShell()); }); widgetUtil(mntmDrawSelectionPtr[0]).addSelectionListener(e -> { diff --git a/src/org/nschmidt/ldparteditor/data/GDataBinary.java b/src/org/nschmidt/ldparteditor/data/GDataBinary.java index 4440381ce..7a1affbc0 100644 --- a/src/org/nschmidt/ldparteditor/data/GDataBinary.java +++ b/src/org/nschmidt/ldparteditor/data/GDataBinary.java @@ -29,6 +29,9 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of */ public final class GDataBinary extends GData { + /** 4 chars = 3 bytes => 64.000 chars = 48.000 bytes */ + private static final int MAX_LENGTH_OF_BASE64_STRING = 64_000; + private final DatFile df; public GDataBinary(String text, DatFile df, GData1 parent) { @@ -43,7 +46,7 @@ public byte[] loadBinary() { final StringBuilder base64Sb = new StringBuilder(); GData gd = this.next; - while (gd != null) { + while (gd != null && base64Sb.length() <= MAX_LENGTH_OF_BASE64_STRING) { final String line = whitespace.matcher(gd.toString()).replaceAll(" ").trim(); //$NON-NLS-1$ if (line.startsWith("0 !: ")) { //$NON-NLS-1$ final String encodedSubstring = line.substring(5); @@ -58,13 +61,84 @@ public byte[] loadBinary() { final String encodedString = base64Sb.toString(); try { - return Base64.getDecoder().decode(encodedString); + // Don't allow more than a constant amount of chars + if (encodedString.length() <= MAX_LENGTH_OF_BASE64_STRING) { + final byte[] preFilteredData = Base64.getDecoder().decode(encodedString); + return filterMaliciousContent(preFilteredData); + } } catch (IllegalArgumentException iae) { NLogger.debug(GDataBinary.class, iae); } return new byte[0]; } + + /** + * Malicious content can be attached to the end of the file after the IEND tag which typically marks the + * end of the image file. We don't want to read or store this! + * @param data the byte array to filter + * @return the filtered array + */ + private byte[] filterMaliciousContent(byte[] data) { + int state = 0; + int length = 0; + int dataLength = 0; + for (byte b : data) { + if (dataLength > 0) { + break; + } + + // Encoders and decoders must treat the codes as fixed binary values, + // not character strings. + switch (b) { + case 73: { // I + if (state == 0) { + state = 1; + } else { + state = 0; + } + + break; + } + case 69: { // E + if (state == 1) { + state = 2; + } else { + state = 0; + } + + break; + } + case 78: { // N + if (state == 2) { + state = 3; + } else { + state = 0; + } + + break; + } + case 68: { // D + if (state == 3) { + // + 4 Byte CRC for IEND + dataLength = Math.min(length + 5, data.length); + } else { + state = 0; + } + + break; + } + default: + // Do nothing. + } + + length++; + } + + final byte[] resultData = new byte[dataLength]; + System.arraycopy(data, 0, resultData, 0, dataLength); + return resultData; + } @Override public void drawGL20(Composite3D c3d) { diff --git a/src/org/nschmidt/ldparteditor/helper/compositetext/DataMetacommandExporter.java b/src/org/nschmidt/ldparteditor/helper/compositetext/DataMetacommandExporter.java index 0d2850fd4..0460a52df 100644 --- a/src/org/nschmidt/ldparteditor/helper/compositetext/DataMetacommandExporter.java +++ b/src/org/nschmidt/ldparteditor/helper/compositetext/DataMetacommandExporter.java @@ -1,5 +1,3 @@ -package org.nschmidt.ldparteditor.helper.compositetext; - /* MIT - License Copyright (c) 2012 - this year, Nils Schmidt @@ -15,11 +13,31 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of 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. */ +package org.nschmidt.ldparteditor.helper.compositetext; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Shell; import org.nschmidt.ldparteditor.data.DatFile; import org.nschmidt.ldparteditor.data.GData; import org.nschmidt.ldparteditor.data.GDataBinary; import org.nschmidt.ldparteditor.helper.math.HashBiMap; +import org.nschmidt.ldparteditor.i18n.I18n; +import org.nschmidt.ldparteditor.logger.NLogger; +import org.nschmidt.ldparteditor.project.Project; + +import de.matthiasmann.twl.util.PNGDecoder; +import de.matthiasmann.twl.util.PNGDecoder.Format; /** * Exports !DATA meta-commands to a file (PNG only) @@ -36,15 +54,73 @@ public enum DataMetacommandExporter { * end line number to export * @param datFile */ - public static void export(int lineStart, int lineEnd, DatFile datFile) { + public static void export(int lineStart, int lineEnd, DatFile datFile, Shell shell) { HashBiMap dpl = datFile.getDrawPerLineNoClone(); lineEnd++; + + boolean noSelection = true; for (int line = lineStart; line < lineEnd; line++) { final GData data = dpl.getValue(line); - if (data instanceof GDataBinary gd) { - // TODO Needs implementation! - System.out.println(data); + if (data instanceof GDataBinary dataMetaTag) { + final byte[] binary = dataMetaTag.loadBinary(); + if (binary.length > 0) { + try (InputStream in = new ByteArrayInputStream(binary)) { + // Link the PNG decoder to this stream + final PNGDecoder decoder = new PNGDecoder(in); + + // Get the width and height of the texture + int width = decoder.getWidth(); + int height = decoder.getHeight(); + + // Decode the PNG file in a ByteBuffer + ByteBuffer buf = ByteBuffer.allocateDirect(4 * width * height); + decoder.decode(buf, width * 4, Format.RGBA); + + // Now try to save the image + FileDialog fd = new FileDialog(shell, SWT.SAVE); + fd.setText(I18n.E3D_SAVE_AS); + fd.setOverwrite(true); + + File f = new File(datFile.getNewName()).getParentFile(); + if (f != null && f.exists()) { + fd.setFilterPath(f.getAbsolutePath()); + } else { + fd.setFilterPath(Project.getLastVisitedPath()); + } + + final String filename = dataMetaTag.toString().substring(8); + fd.setFileName(filename); + + String[] filterExt = { "*.png", "*.*" }; //$NON-NLS-1$ //$NON-NLS-2$ + fd.setFilterExtensions(filterExt); + String[] filterNames = {I18n.E3D_PORTABLE_NETWORK_GRAPHICS, I18n.E3D_ALL_FILES}; + fd.setFilterNames(filterNames); + + String selected = fd.open(); + + if (selected != null) { + try (OutputStream fos = new FileOutputStream(selected)) { + fos.write(binary); + } + } + + noSelection = false; + } catch (IOException e) { + NLogger.debug(DataMetacommandExporter.class, e); + MessageBox messageBoxError = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK); + messageBoxError.setText(I18n.DIALOG_ERROR); + messageBoxError.setMessage(I18n.EDITORTEXT_DATA_ERROR_EXPORT); + messageBoxError.open(); + } + } } } + + if (noSelection) { + MessageBox messageBoxError = new MessageBox(shell, SWT.ICON_WARNING | SWT.OK); + messageBoxError.setText(I18n.DIALOG_WARNING); + messageBoxError.setMessage(I18n.EDITORTEXT_DATA_NO_SELECTION); + messageBoxError.open(); + } } } diff --git a/src/org/nschmidt/ldparteditor/i18n/I18n.java b/src/org/nschmidt/ldparteditor/i18n/I18n.java index 776452437..04fc8128f 100644 --- a/src/org/nschmidt/ldparteditor/i18n/I18n.java +++ b/src/org/nschmidt/ldparteditor/i18n/I18n.java @@ -758,7 +758,8 @@ private static void adjust() { // Calculate line offset public static final String EDITORTEXT_COMMENT = EDITORTEXT.getString(getProperty()); public static final String EDITORTEXT_COMPILE = EDITORTEXT.getString(getProperty()); public static final String EDITORTEXT_COMPILE_CSG = EDITORTEXT.getString(getProperty()); - public static final String EDITORTEXT_DATA_ERROR = EDITORTEXT.getString(getProperty()); + public static final String EDITORTEXT_DATA_ERROR_EXPORT = EDITORTEXT.getString(getProperty()); + public static final String EDITORTEXT_DATA_ERROR_IMPORT = EDITORTEXT.getString(getProperty()); public static final String EDITORTEXT_DATA_EXPORT = EDITORTEXT.getString(getProperty()); public static final String EDITORTEXT_DATA_IMPORT = EDITORTEXT.getString(getProperty()); public static final String EDITORTEXT_DATA_NO_SELECTION = EDITORTEXT.getString(getProperty()); diff --git a/src/org/nschmidt/ldparteditor/i18n/TextEditor.properties b/src/org/nschmidt/ldparteditor/i18n/TextEditor.properties index 948640db6..a1a7d3594 100644 --- a/src/org/nschmidt/ldparteditor/i18n/TextEditor.properties +++ b/src/org/nschmidt/ldparteditor/i18n/TextEditor.properties @@ -3,10 +3,11 @@ COLOUR_2 = Colour [{0}] COMMENT = Toggle Comment COMPILE = Update/Compile linked Subfile Data COMPILE_CSG = Optimize CSG Triangulation...\n(you can skip this) -DATA_ERROR = The file was not a valid PNG image. +DATA_ERROR_EXPORT = A valid PNG image could not be created. +DATA_ERROR_IMPORT = A valid !DATA tag could not be created. DATA_EXPORT = Export !DATA as PNG-Image (secure) DATA_IMPORT = Import PNG-Image as !DATA -DATA_NO_SELECTION = No single !DATA line was selected. Nothing to export. +DATA_NO_SELECTION = No single valid !DATA line was selected or the selected data size was too big (limit: 45KB). Nothing to export. DRAW_SELECTION = Draw Selection DRAW_UNTIL_SELECTION = Draw Until Selection DUPLICATE = duplicate