├── .gitignore ├── icon.ico ├── splash.png ├── org.json.jar ├── fluent-hc-4.3.3.jar ├── httpclient-4.3.3.jar ├── httpcore-4.3.2.jar ├── httpmime-4.3.3.jar ├── commons-codec-1.6.jar ├── commons-logging-1.1.3.jar ├── httpclient-cache-4.3.3.jar ├── dist └── thumbnailguihacker2.1.1.jar ├── src └── apu │ └── scratch │ └── hax │ ├── watermark.png │ ├── ProgressFileEntity.java │ ├── GuiHackerProjectsView.java │ ├── GuiHackerLogin.java │ ├── GuiHackerBackend.java │ ├── ThumbnailGuiHacker.java │ └── GuiHackerFileChooser.java ├── Readme.md ├── .project ├── LICENSE ├── .settings └── org.eclipse.jdt.core.prefs └── .classpath /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | build/ 3 | launch4j.xml -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/icon.ico -------------------------------------------------------------------------------- /splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/splash.png -------------------------------------------------------------------------------- /org.json.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/org.json.jar -------------------------------------------------------------------------------- /fluent-hc-4.3.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/fluent-hc-4.3.3.jar -------------------------------------------------------------------------------- /httpclient-4.3.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/httpclient-4.3.3.jar -------------------------------------------------------------------------------- /httpcore-4.3.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/httpcore-4.3.2.jar -------------------------------------------------------------------------------- /httpmime-4.3.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/httpmime-4.3.3.jar -------------------------------------------------------------------------------- /commons-codec-1.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/commons-codec-1.6.jar -------------------------------------------------------------------------------- /commons-logging-1.1.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/commons-logging-1.1.3.jar -------------------------------------------------------------------------------- /httpclient-cache-4.3.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/httpclient-cache-4.3.3.jar -------------------------------------------------------------------------------- /dist/thumbnailguihacker2.1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/dist/thumbnailguihacker2.1.1.jar -------------------------------------------------------------------------------- /src/apu/scratch/hax/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaApuTurkUltra/Scratch-Thumbnail-Changer/HEAD/src/apu/scratch/hax/watermark.png -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # MegaApuTurkUltra's Scratch Thumbnail Changer # 2 | Allows arbitrary thumbnails (of any supported image format including GIF) to be set for projects on [MIT Scratch](https://scratch.mit.edu). -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Thumbnail GUI Hacker 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ======= The MegaApuTurkUltra Swag License ======= 2 | 3 | >>> Conditions for use of this software: <<< 4 | 5 | * You MAY NOT use this software for evil, 6 | including but not limited to purposes of 7 | malicious intent, or gaining access to systems 8 | you do not have permission to access. 9 | 10 | If you do not wish to comply with these terms, go 11 | use some other software please (or forge requests 12 | yourself) 13 | 14 | ================================================= -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=1.7 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 11 | org.eclipse.jdt.core.compiler.source=1.7 12 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/apu/scratch/hax/ProgressFileEntity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package apu.scratch.hax; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.OutputStream; 9 | 10 | import org.apache.http.entity.ContentType; 11 | 12 | /** 13 | * @see http://stackoverflow.com/a/8475006/1021196 14 | */ 15 | public class ProgressFileEntity extends org.apache.http.entity.FileEntity { 16 | 17 | public static interface ProgressCallback { 18 | public void updateProgress(int progress); 19 | } 20 | 21 | class OutputStreamProgress extends OutputStream { 22 | private final OutputStream outstream; 23 | private volatile long bytesWritten = 0; 24 | private ProgressCallback callback; 25 | private ProgressFileEntity entity; 26 | 27 | public OutputStreamProgress(OutputStream outstream, 28 | ProgressCallback callback, ProgressFileEntity entity) { 29 | this.outstream = outstream; 30 | this.callback = callback; 31 | this.entity = entity; 32 | } 33 | 34 | @Override 35 | public void write(int b) throws IOException { 36 | outstream.write(b); 37 | bytesWritten++; 38 | updateProgress(); 39 | } 40 | 41 | @Override 42 | public void write(byte[] b) throws IOException { 43 | outstream.write(b); 44 | bytesWritten += b.length; 45 | updateProgress(); 46 | } 47 | 48 | @Override 49 | public void write(byte[] b, int off, int len) throws IOException { 50 | outstream.write(b, off, len); 51 | bytesWritten += len; 52 | updateProgress(); 53 | } 54 | 55 | @Override 56 | public void flush() throws IOException { 57 | outstream.flush(); 58 | } 59 | 60 | @Override 61 | public void close() throws IOException { 62 | outstream.close(); 63 | } 64 | 65 | public long getWrittenLength() { 66 | return bytesWritten; 67 | } 68 | 69 | private void updateProgress() { 70 | callback.updateProgress((int) (100 * bytesWritten / entity 71 | .getContentLength())); 72 | } 73 | } 74 | 75 | private OutputStreamProgress outstream; 76 | private ProgressCallback callback; 77 | 78 | public ProgressFileEntity(File file, String contentType, 79 | ProgressCallback callback) { 80 | super(file, ContentType.create(contentType)); 81 | this.callback = callback; 82 | } 83 | 84 | @Override 85 | public void writeTo(OutputStream outstream) throws IOException { 86 | this.outstream = new OutputStreamProgress(outstream, callback, this); 87 | super.writeTo(this.outstream); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/apu/scratch/hax/GuiHackerProjectsView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package apu.scratch.hax; 5 | 6 | import java.awt.BorderLayout; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.ActionListener; 9 | 10 | import javax.swing.AbstractListModel; 11 | import javax.swing.DefaultListModel; 12 | import javax.swing.JButton; 13 | import javax.swing.JLabel; 14 | import javax.swing.JList; 15 | import javax.swing.JPanel; 16 | import javax.swing.JScrollPane; 17 | import javax.swing.ListSelectionModel; 18 | 19 | /** 20 | * @author "MegaApuTurkUltra" 21 | * 22 | */ 23 | public class GuiHackerProjectsView extends JPanel { 24 | public static class Project { 25 | public String title; 26 | public int id; 27 | 28 | public String toString() { 29 | return title; 30 | } 31 | 32 | public Project(String title, int id) { 33 | this.title = title; 34 | this.id = id; 35 | } 36 | } 37 | 38 | private static final long serialVersionUID = -8777426689312871264L; 39 | public JList list; 40 | public DefaultListModel listModel; 41 | public JButton btnChangeIt; 42 | private JScrollPane scrollPane; 43 | 44 | /** 45 | * Create the panel. 46 | */ 47 | public GuiHackerProjectsView() { 48 | setLayout(new BorderLayout(0, 0)); 49 | 50 | JLabel lblSelectAProject = new JLabel( 51 | "Select a project to change its thumbnail"); 52 | add(lblSelectAProject, BorderLayout.NORTH); 53 | 54 | btnChangeIt = new JButton("Change it!"); 55 | btnChangeIt.addActionListener(new ActionListener() { 56 | public void actionPerformed(ActionEvent e) { 57 | if (list.getSelectedIndex() == -1) 58 | return; 59 | Project proj = list.getSelectedValue(); 60 | ThumbnailGuiHacker.INSTANCE.goToFileChooser(proj); 61 | } 62 | }); 63 | btnChangeIt.setEnabled(false); 64 | add(btnChangeIt, BorderLayout.SOUTH); 65 | listModel = new DefaultListModel(); 66 | 67 | scrollPane = new JScrollPane(); 68 | add(scrollPane, BorderLayout.CENTER); 69 | 70 | list = new JList(); 71 | list.setEnabled(false); 72 | list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 73 | list.setModel(new AbstractListModel() { 74 | private static final long serialVersionUID = -8079045657394409854L; 75 | @Override 76 | public int getSize() { 77 | return 1; 78 | } 79 | @Override 80 | public Project getElementAt(int index) { 81 | return new Project("Loading...", -1); 82 | } 83 | }); 84 | scrollPane.setViewportView(list); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/apu/scratch/hax/GuiHackerLogin.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package apu.scratch.hax; 5 | 6 | import java.awt.Font; 7 | import java.awt.GridBagConstraints; 8 | import java.awt.GridBagLayout; 9 | import java.awt.Insets; 10 | import java.awt.event.ActionEvent; 11 | import java.awt.event.ActionListener; 12 | 13 | import javax.swing.JButton; 14 | import javax.swing.JLabel; 15 | import javax.swing.JOptionPane; 16 | import javax.swing.JPanel; 17 | import javax.swing.JPasswordField; 18 | import javax.swing.JTextArea; 19 | import javax.swing.JTextField; 20 | import javax.swing.SwingUtilities; 21 | import javax.swing.UIManager; 22 | 23 | /** 24 | * Login screen 25 | * 26 | * @author "MegaApuTurkUltra" 27 | * 28 | */ 29 | public class GuiHackerLogin extends JPanel { 30 | private static final long serialVersionUID = -6357875066223412629L; 31 | private JTextField username; 32 | private JPasswordField password; 33 | private JButton btnLogIn; 34 | 35 | /** 36 | * Create the panel. 37 | */ 38 | public GuiHackerLogin() { 39 | GridBagLayout gridBagLayout = new GridBagLayout(); 40 | gridBagLayout.columnWidths = new int[] { 0, 0, 0 }; 41 | gridBagLayout.rowHeights = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; 42 | gridBagLayout.columnWeights = new double[] { 1.0, 1.0, Double.MIN_VALUE }; 43 | gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 44 | 1.0, 0.0, Double.MIN_VALUE }; 45 | setLayout(gridBagLayout); 46 | 47 | JLabel lblLogInTo = new JLabel("Log in to your Scratch Account"); 48 | lblLogInTo.setFont(new Font("Tahoma", Font.PLAIN, 20)); 49 | GridBagConstraints gbc_lblLogInTo = new GridBagConstraints(); 50 | gbc_lblLogInTo.gridwidth = 2; 51 | gbc_lblLogInTo.insets = new Insets(0, 0, 5, 0); 52 | gbc_lblLogInTo.gridx = 0; 53 | gbc_lblLogInTo.gridy = 0; 54 | add(lblLogInTo, gbc_lblLogInTo); 55 | 56 | JLabel lblThumbnailHacker = new JLabel("Thumbnail Hacker - A swaggy program by MegaApuTurkUltra"); 57 | GridBagConstraints gbc_lblThumbnailHacker = new GridBagConstraints(); 58 | gbc_lblThumbnailHacker.gridwidth = 2; 59 | gbc_lblThumbnailHacker.insets = new Insets(0, 0, 5, 5); 60 | gbc_lblThumbnailHacker.gridx = 0; 61 | gbc_lblThumbnailHacker.gridy = 1; 62 | add(lblThumbnailHacker, gbc_lblThumbnailHacker); 63 | 64 | JLabel lblUsername = new JLabel("Username:"); 65 | GridBagConstraints gbc_lblUsername = new GridBagConstraints(); 66 | gbc_lblUsername.anchor = GridBagConstraints.EAST; 67 | gbc_lblUsername.insets = new Insets(0, 0, 5, 5); 68 | gbc_lblUsername.gridx = 0; 69 | gbc_lblUsername.gridy = 2; 70 | add(lblUsername, gbc_lblUsername); 71 | 72 | username = new JTextField(); 73 | GridBagConstraints gbc_username = new GridBagConstraints(); 74 | gbc_username.insets = new Insets(0, 0, 5, 0); 75 | gbc_username.fill = GridBagConstraints.HORIZONTAL; 76 | gbc_username.gridx = 1; 77 | gbc_username.gridy = 2; 78 | add(username, gbc_username); 79 | username.setColumns(10); 80 | 81 | JLabel lblPassword = new JLabel("Password:"); 82 | GridBagConstraints gbc_lblPassword = new GridBagConstraints(); 83 | gbc_lblPassword.anchor = GridBagConstraints.EAST; 84 | gbc_lblPassword.insets = new Insets(0, 0, 5, 5); 85 | gbc_lblPassword.gridx = 0; 86 | gbc_lblPassword.gridy = 3; 87 | add(lblPassword, gbc_lblPassword); 88 | 89 | password = new JPasswordField(); 90 | GridBagConstraints gbc_password = new GridBagConstraints(); 91 | gbc_password.insets = new Insets(0, 0, 5, 0); 92 | gbc_password.fill = GridBagConstraints.HORIZONTAL; 93 | gbc_password.gridx = 1; 94 | gbc_password.gridy = 3; 95 | add(password, gbc_password); 96 | 97 | btnLogIn = new JButton("Log In"); 98 | btnLogIn.addActionListener(new ActionListener() { 99 | public void actionPerformed(ActionEvent e) { 100 | SwingUtilities.invokeLater(new Runnable() { 101 | @Override 102 | public void run() { 103 | username.setEnabled(false); 104 | password.setEnabled(false); 105 | btnLogIn.setEnabled(false); 106 | } 107 | }); 108 | new Thread(new Runnable(){ 109 | @Override 110 | public void run() { 111 | boolean success = ThumbnailGuiHacker.INSTANCE 112 | .initAndLogin(username.getText(), 113 | password.getPassword()); 114 | boolean doCont = success; 115 | if (!success) { 116 | doCont = JOptionPane.showConfirmDialog( 117 | ThumbnailGuiHacker.INSTANCE, 118 | "Login might have failed. Continue?", 119 | "Message", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION; 120 | } 121 | if (!doCont) { 122 | username.setEnabled(true); 123 | password.setEnabled(true); 124 | btnLogIn.setEnabled(true); 125 | } else { 126 | ThumbnailGuiHacker.INSTANCE.goToProjects(); 127 | } 128 | } 129 | }).start(); 130 | } 131 | }); 132 | btnLogIn.setFont(new Font("Tahoma", Font.PLAIN, 20)); 133 | GridBagConstraints gbc_btnLogIn = new GridBagConstraints(); 134 | gbc_btnLogIn.insets = new Insets(0, 0, 5, 0); 135 | gbc_btnLogIn.fill = GridBagConstraints.BOTH; 136 | gbc_btnLogIn.gridx = 1; 137 | gbc_btnLogIn.gridy = 5; 138 | add(btnLogIn, gbc_btnLogIn); 139 | 140 | JTextArea txtrNoteYourLogin = new JTextArea(); 141 | txtrNoteYourLogin.setFont(new Font("Monospaced", Font.ITALIC, 13)); 142 | txtrNoteYourLogin 143 | .setText("Note: Your login info is not collected in any way. It is discarded once this application closes"); 144 | txtrNoteYourLogin.setBackground(UIManager.getColor("Panel.background")); 145 | txtrNoteYourLogin.setEnabled(false); 146 | txtrNoteYourLogin.setEditable(false); 147 | txtrNoteYourLogin.setWrapStyleWord(true); 148 | txtrNoteYourLogin.setLineWrap(true); 149 | GridBagConstraints gbc_txtrNoteYourLogin = new GridBagConstraints(); 150 | gbc_txtrNoteYourLogin.insets = new Insets(0, 0, 5, 0); 151 | gbc_txtrNoteYourLogin.gridwidth = 2; 152 | gbc_txtrNoteYourLogin.fill = GridBagConstraints.BOTH; 153 | gbc_txtrNoteYourLogin.gridx = 0; 154 | gbc_txtrNoteYourLogin.gridy = 6; 155 | add(txtrNoteYourLogin, gbc_txtrNoteYourLogin); 156 | 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/apu/scratch/hax/GuiHackerBackend.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package apu.scratch.hax; 5 | 6 | import java.io.File; 7 | import java.io.InputStream; 8 | 9 | import javax.net.ssl.SSLContext; 10 | import javax.net.ssl.TrustManager; 11 | import javax.net.ssl.X509TrustManager; 12 | 13 | import org.apache.http.HttpHost; 14 | import org.apache.http.client.CookieStore; 15 | import org.apache.http.client.config.CookieSpecs; 16 | import org.apache.http.client.config.RequestConfig; 17 | import org.apache.http.client.methods.CloseableHttpResponse; 18 | import org.apache.http.client.methods.HttpUriRequest; 19 | import org.apache.http.client.methods.RequestBuilder; 20 | import org.apache.http.cookie.Cookie; 21 | import org.apache.http.entity.StringEntity; 22 | import org.apache.http.impl.client.BasicCookieStore; 23 | import org.apache.http.impl.client.CloseableHttpClient; 24 | import org.apache.http.impl.client.HttpClientBuilder; 25 | import org.apache.http.impl.client.HttpClients; 26 | import org.apache.http.impl.cookie.BasicClientCookie; 27 | import org.json.JSONArray; 28 | import org.json.JSONObject; 29 | 30 | import apu.scratch.hax.ProgressFileEntity.ProgressCallback; 31 | 32 | /** 33 | * Backend to handle HTTP 34 | * 35 | * @author "MegaApuTurkUltra" 36 | */ 37 | public class GuiHackerBackend { 38 | static RequestConfig globalConfig; 39 | static CookieStore cookieStore; 40 | static CloseableHttpClient httpClient; 41 | static CloseableHttpResponse resp; 42 | static String csrfToken; 43 | 44 | private static boolean useProxy = false; 45 | 46 | public static void reset() { 47 | try { 48 | csrfToken = null; 49 | if (cookieStore != null) 50 | cookieStore.clear(); 51 | if (resp != null) 52 | resp.close(); 53 | if (httpClient != null) 54 | httpClient.close(); 55 | } catch (Exception e) { 56 | e.printStackTrace(); 57 | // ignore. HttpClient was probably in the middle of something 58 | // or not fully initialized. We can let GC handle this 59 | } 60 | System.gc(); 61 | } 62 | 63 | /** 64 | * DO NOT USE SUPER DANGEROUS 65 | */ 66 | public static SSLContext bypassSSLCertCheck() throws Exception { 67 | /*SSLContext sslContext = SSLContext.getInstance("SSL"); 68 | sslContext.init(null, new TrustManager[] { new X509TrustManager() { 69 | public X509Certificate[] getAcceptedIssuers() { 70 | return null; 71 | } 72 | 73 | public void checkClientTrusted(X509Certificate[] certs, 74 | String authType) { 75 | } 76 | 77 | public void checkServerTrusted(X509Certificate[] certs, 78 | String authType) { 79 | } 80 | } }, new SecureRandom()); 81 | return sslContext;*/ return null; 82 | } 83 | 84 | public static void init() throws Exception { 85 | RequestConfig.Builder configBuilder = RequestConfig.custom() 86 | .setCookieSpec(CookieSpecs.BROWSER_COMPATIBILITY) 87 | .setSocketTimeout(0).setConnectionRequestTimeout(0) 88 | .setConnectTimeout(0); 89 | if (useProxy) { 90 | configBuilder.setProxy(new HttpHost("localhost", 8888)); 91 | } 92 | 93 | globalConfig = configBuilder.build(); 94 | 95 | if (!useProxy) { 96 | cookieStore = new BasicCookieStore(); 97 | } 98 | BasicClientCookie lang = new BasicClientCookie("scratchlanguage", "en"); 99 | lang.setDomain(".scratch.mit.edu"); 100 | lang.setPath("/"); 101 | cookieStore.addCookie(lang); 102 | // hacks activated in 3...2...1.. 103 | 104 | HttpClientBuilder clientBuilder = HttpClients 105 | .custom() 106 | .setDefaultRequestConfig(globalConfig) 107 | .setUserAgent( 108 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36") 109 | .setDefaultCookieStore(cookieStore); 110 | if (useProxy) { 111 | clientBuilder.setSslcontext(bypassSSLCertCheck()); 112 | } 113 | httpClient = clientBuilder.build(); 114 | } 115 | 116 | public static void login(String username, char[] password) throws Exception { 117 | System.out.print("Requesting index page..."); 118 | HttpUriRequest index = RequestBuilder.get() 119 | .setUri("https://scratch.mit.edu/") 120 | .addHeader("Accept", "text/html") 121 | .addHeader("Referer", "https://scratch.mit.edu").build(); 122 | resp = httpClient.execute(index); 123 | System.out.println(resp.getStatusLine()); 124 | resp.close(); 125 | 126 | System.out.print("Requesting CSRF token..."); 127 | HttpUriRequest csrf = RequestBuilder.get() 128 | .setUri("https://scratch.mit.edu/csrf_token/") 129 | .addHeader("Accept", "*/*") 130 | .addHeader("Referer", "https://scratch.mit.edu") 131 | .addHeader("X-Requested-With", "XMLHttpRequest").build(); 132 | resp = httpClient.execute(csrf); 133 | System.out.println(resp.getStatusLine()); 134 | resp.close(); 135 | 136 | for (Cookie c : cookieStore.getCookies()) { 137 | if (c.getName().equals("scratchcsrftoken")) { 138 | csrfToken = c.getValue(); 139 | } 140 | } 141 | System.out.println("Found CSRF: " + csrfToken); 142 | 143 | System.out.print("Logging in..."); 144 | JSONObject loginObj = new JSONObject(); 145 | loginObj.put("username", username); 146 | loginObj.put("password", new String(password)); 147 | loginObj.put("captcha_challenge", ""); 148 | loginObj.put("captcha_response", ""); 149 | loginObj.put("embed_captcha", false); 150 | loginObj.put("timezone", "America/New_York"); 151 | loginObj.put("csrfmiddlewaretoken", csrfToken); 152 | HttpUriRequest login = RequestBuilder 153 | .post() 154 | .setUri("https://scratch.mit.edu/login/") 155 | .addHeader("Accept", 156 | "application/json, text/javascript, */*; q=0.01") 157 | .addHeader("Referer", "https://scratch.mit.edu") 158 | .addHeader("Origin", "https://scratch.mit.edu") 159 | .addHeader("Content-Type", "application/json") 160 | .addHeader("X-Requested-With", "XMLHttpRequest") 161 | .addHeader("X-CSRFToken", csrfToken) 162 | .setEntity(new StringEntity(loginObj.toString())).build(); 163 | resp = httpClient.execute(login); 164 | System.out.println(resp.getStatusLine()); 165 | StringBuffer loginResp = new StringBuffer(); 166 | InputStream in = resp.getEntity().getContent(); 167 | int i; 168 | while ((i = in.read()) != -1) 169 | loginResp.append((char) i); 170 | in.close(); 171 | resp.close(); 172 | System.out.println(loginResp.toString()); 173 | 174 | JSONArray response = new JSONArray(loginResp.toString()); 175 | JSONObject obj = response.getJSONObject(0); 176 | String loginMessage = "Unknown"; 177 | if (obj.has("msg")) 178 | loginMessage = obj.getString("msg"); 179 | if (obj.getInt("success") != 1) { 180 | throw new Exception( 181 | "Login might have failed\nScratch returned the message:\n\"" 182 | + loginMessage + "\""); 183 | } 184 | } 185 | 186 | public static JSONArray getProjects() throws Exception { 187 | System.out.print("Loading projects..."); 188 | HttpUriRequest projects = RequestBuilder.get() 189 | .setUri("https://scratch.mit.edu/site-api/projects/all/") 190 | .addHeader("Accept", "*/*") 191 | .addHeader("Referer", "https://scratch.mit.edu") 192 | .addHeader("X-Requested-With", "XMLHttpRequest").build(); 193 | resp = httpClient.execute(projects); 194 | System.out.println(resp.getStatusLine()); 195 | resp.close(); 196 | StringBuffer projectsResp = new StringBuffer(); 197 | InputStream in = resp.getEntity().getContent(); 198 | int i; 199 | while ((i = in.read()) != -1) 200 | projectsResp.append((char) i); 201 | in.close(); 202 | resp.close(); 203 | return new JSONArray(projectsResp.toString()); 204 | } 205 | 206 | public static void hackThumbnail(int projectId, File thumbnail, 207 | String mimeType) throws Exception { 208 | System.out.print("Uploading thumbnail..."); 209 | 210 | // debug only, do not enable 211 | // useProxy = true; 212 | // init(); 213 | 214 | // the ST is sneaky :P 215 | BasicClientCookie pid = new BasicClientCookie("projectId", 216 | Integer.toString(projectId)); 217 | pid.setDomain(".scratch.mit.edu"); 218 | pid.setPath("/"); 219 | cookieStore.addCookie(pid); 220 | 221 | // even more sneaky... 222 | System.out.print("Refinding CSRF..."); 223 | for (Cookie c : cookieStore.getCookies()) { 224 | if (c.getName().equals("scratchcsrftoken")) { 225 | csrfToken = c.getValue(); 226 | } 227 | } 228 | System.out.println("Found CSRF: " + csrfToken); 229 | 230 | HttpUriRequest thumb = RequestBuilder 231 | .post() 232 | .setUri("https://scratch.mit.edu/internalapi/project" 233 | + "/thumbnail/" + projectId + "/set/?v=v440.3&_rnd=" 234 | + Math.random()) 235 | .addHeader( 236 | "Referer", 237 | "https://cdn.scratch.mit.edu/scratchr2/static/__61560e08176805f6f84b8d1dd737d59b__/Scratch.swf") 238 | .addHeader("Content-Type", mimeType) 239 | .addHeader("X-CSRFToken", csrfToken) 240 | .addHeader("X-Requested-With", "ShockwaveFlash/19.0.0.226") 241 | .addHeader("Origin", "https://cdn.scratch.mit.edu") 242 | .addHeader("Accept", "*/*") 243 | .addHeader("Accept-Language", "en-US,en;q=0.8") 244 | .setEntity( 245 | new ProgressFileEntity(thumbnail, "image/png", 246 | new ProgressCallback() { 247 | @Override 248 | public void updateProgress(int progress) { 249 | if(ThumbnailGuiHacker.INSTANCE == null){ 250 | System.out.print("\rUpload: " + progress + "%"); 251 | return; 252 | } 253 | ThumbnailGuiHacker.INSTANCE 254 | .setProgress(progress); 255 | } 256 | })).build(); 257 | resp = httpClient.execute(thumb); 258 | System.out.println(resp.getStatusLine()); 259 | int statusCode = resp.getStatusLine().getStatusCode(); 260 | InputStream in = resp.getEntity().getContent(); 261 | int i; 262 | while ((i = in.read()) != -1) { 263 | System.out.print((char) i); 264 | } 265 | resp.close(); 266 | if (statusCode != 200) { 267 | throw new IllegalStateException("Response status is " + statusCode); 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/apu/scratch/hax/ThumbnailGuiHacker.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package apu.scratch.hax; 5 | 6 | import java.awt.BorderLayout; 7 | import java.awt.Dialog.ModalityType; 8 | import java.awt.Window; 9 | import java.io.File; 10 | import java.io.PrintWriter; 11 | import java.io.StringWriter; 12 | import java.lang.Thread.UncaughtExceptionHandler; 13 | import java.lang.reflect.InvocationTargetException; 14 | 15 | import javax.swing.JDialog; 16 | import javax.swing.JFrame; 17 | import javax.swing.JOptionPane; 18 | import javax.swing.JPanel; 19 | import javax.swing.JProgressBar; 20 | import javax.swing.SwingUtilities; 21 | import javax.swing.UIManager; 22 | import javax.swing.border.EmptyBorder; 23 | 24 | import org.json.JSONArray; 25 | import org.json.JSONObject; 26 | 27 | import apu.scratch.hax.GuiHackerProjectsView.Project; 28 | 29 | /** 30 | * A GUI hacker for changing thumbnails 31 | * 32 | * @author "MegaApuTurkUltra" 33 | * 34 | */ 35 | public class ThumbnailGuiHacker extends JFrame { 36 | private static final long serialVersionUID = 478186628181192812L; 37 | private JPanel contentPane; 38 | private GuiHackerLogin login; 39 | private GuiHackerProjectsView projects; 40 | private GuiHackerFileChooser chooser; 41 | public static volatile ThumbnailGuiHacker INSTANCE; 42 | public static Project selectedProject; 43 | static JDialog loading; 44 | static JProgressBar loadingP; 45 | static boolean guiMode; 46 | 47 | static { 48 | Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { 49 | @Override 50 | public void uncaughtException(Thread t, Throwable e) { 51 | System.err.println("Uncaught exception on thread: " + t); 52 | e.printStackTrace(); 53 | if (ThumbnailGuiHacker.INSTANCE != null) { 54 | ThumbnailGuiHacker.INSTANCE.showExceptionDialog(e); 55 | } 56 | } 57 | }); 58 | } 59 | 60 | /** 61 | * Launch the application. 62 | */ 63 | public static void main(String[] args) { 64 | if(args.length == 0){ 65 | guiMode = true; 66 | guiMain(); 67 | return; 68 | } 69 | guiMode = false; 70 | String username = null, password = null, projectId = null, file = null; 71 | for(int i = 0; i + 1 < args.length; i++){ 72 | if(args[i].equals("--username")){ 73 | username = args[i + 1]; 74 | } else if(args[i].equals("--password")){ 75 | password = args[i + 1]; 76 | } else if(args[i].equals("--projectId")){ 77 | projectId = args[i + 1]; 78 | } else if(args[i].equals("--file")){ 79 | file = args[i + 1]; 80 | } 81 | } 82 | if(username == null || password == null || projectId == null || file == null){ 83 | System.out.println("Usage: java -jar thumbnailguihacker.jar --username --password --projectId --file "); 84 | System.exit(-1); 85 | } 86 | 87 | GuiHackerBackend.reset(); 88 | try { 89 | GuiHackerBackend.init(); 90 | GuiHackerBackend.login(username, password.toCharArray()); 91 | 92 | String mime = "image/png"; 93 | String ext = file.substring(file.lastIndexOf('.') + 1); 94 | if(ext.equals("gif")){ 95 | mime = "image/gif"; 96 | } else if(ext.equals("jpg") || ext.equals("jpeg")){ 97 | mime = "image/jpeg"; 98 | } 99 | 100 | GuiHackerBackend.hackThumbnail(Integer.parseInt(projectId), 101 | new File(file), mime); 102 | System.out.println("\nDone"); 103 | } catch (Exception e) { 104 | e.printStackTrace(); 105 | } 106 | } 107 | 108 | public static void guiMain(){ 109 | try { 110 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 111 | 112 | SwingUtilities.invokeAndWait(new Runnable() { 113 | @Override 114 | public void run() { 115 | loading = new JDialog(null, 116 | "Super swaggy hacks in progress...", 117 | ModalityType.MODELESS); 118 | loading.setResizable(false); 119 | loading.setLayout(new BorderLayout()); 120 | loading.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); 121 | loadingP = new JProgressBar(); 122 | loadingP.setIndeterminate(true); 123 | loading.add(loadingP, BorderLayout.CENTER); 124 | loading.setSize(300, 60); 125 | loading.setLocationRelativeTo(null); 126 | loading.setType(Window.Type.UTILITY); 127 | loading.setVisible(true); 128 | } 129 | }); 130 | } catch (Exception e) { 131 | e.printStackTrace(); 132 | } 133 | try { 134 | Thread.sleep(500); 135 | } catch (InterruptedException e1) { 136 | e1.printStackTrace(); 137 | } 138 | SwingUtilities.invokeLater(new Runnable() { 139 | public void run() { 140 | try { 141 | INSTANCE = new ThumbnailGuiHacker(); 142 | loading.setVisible(false); 143 | INSTANCE.setVisible(true); 144 | } catch (Exception e) { 145 | e.printStackTrace(); 146 | } 147 | } 148 | }); 149 | } 150 | 151 | /** 152 | * Create the frame. 153 | */ 154 | public ThumbnailGuiHacker() { 155 | setTitle("MegaApuTurkUltra - Thumbnail GUI Hacker"); 156 | setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 157 | setSize(600, 600); 158 | contentPane = new JPanel(); 159 | contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); 160 | contentPane.setLayout(new BorderLayout(0, 0)); 161 | setContentPane(contentPane); 162 | 163 | login = new GuiHackerLogin(); 164 | contentPane.add(login, BorderLayout.CENTER); 165 | 166 | projects = new GuiHackerProjectsView(); 167 | chooser = new GuiHackerFileChooser(); 168 | 169 | setLocationRelativeTo(null); 170 | } 171 | 172 | public boolean initAndLogin(String username, char[] password) { 173 | setLoadingState(true); 174 | GuiHackerBackend.reset(); 175 | try { 176 | GuiHackerBackend.init(); 177 | GuiHackerBackend.login(username, password); 178 | return true; 179 | } catch (Exception e) { 180 | e.printStackTrace(); 181 | setLoadingState(false); 182 | showExceptionDialog(e); 183 | } finally { 184 | setLoadingState(false); 185 | } 186 | return false; 187 | } 188 | 189 | public void setLoadingState(final boolean state) { 190 | try { 191 | Runnable doRun = new Runnable() { 192 | @Override 193 | public void run() { 194 | loading.setLocationRelativeTo(ThumbnailGuiHacker.this); 195 | loading.setVisible(state); 196 | } 197 | }; 198 | if (SwingUtilities.isEventDispatchThread()) 199 | doRun.run(); 200 | else 201 | SwingUtilities.invokeAndWait(doRun); 202 | } catch (InvocationTargetException e) { 203 | e.printStackTrace(); 204 | } catch (InterruptedException e) { 205 | e.printStackTrace(); 206 | } 207 | } 208 | 209 | public void showExceptionDialog(Throwable e) { 210 | String message = null; 211 | int x = JOptionPane.showOptionDialog(this, "An error has occurred: " 212 | + (((message = e.getMessage()) == null) ? "Unknown" : message), 213 | "Error", JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, 214 | null, new Object[] { "OK", "View Details", "Exit" }, 0); 215 | if (x == 1) { 216 | StringWriter stack = new StringWriter(); 217 | e.printStackTrace(new PrintWriter(stack)); 218 | JOptionPane.showMessageDialog(this, stack.toString(), 219 | "Error Details", JOptionPane.ERROR_MESSAGE); 220 | showExceptionDialog(e); 221 | } 222 | if (x == 2) 223 | System.exit(-1); 224 | } 225 | 226 | public void success() { 227 | setLoadingState(false); 228 | int x = JOptionPane 229 | .showOptionDialog( 230 | this, 231 | "Success! Thumbnail has been changed.\nRemember that opening " 232 | + "the editor on your project\nwill change the thumbnail again.", 233 | "Swag", JOptionPane.YES_NO_OPTION, 234 | JOptionPane.INFORMATION_MESSAGE, null, 235 | new Object[] { "Got it; Exit this app", 236 | "Change another one!" }, 0); 237 | if (x == 0) 238 | System.exit(0); 239 | else 240 | goToProjects(); 241 | } 242 | 243 | public boolean warnAnnoyingGif() { 244 | return JOptionPane 245 | .showOptionDialog( 246 | this, 247 | "Warning: That GIF has a framerate that's too high, and may be annoying.", 248 | "Warning", JOptionPane.YES_NO_OPTION, 249 | JOptionPane.WARNING_MESSAGE, null, new Object[] { 250 | "I'll choose another GIF", 251 | "I understand, proceed" }, 0) == 1; 252 | } 253 | 254 | public void goToProjects() { 255 | SwingUtilities.invokeLater(new Runnable() { 256 | @Override 257 | public void run() { 258 | setTitle("Thumbnail Hacker - Select a project:"); 259 | contentPane.remove(chooser); 260 | contentPane.remove(login); 261 | contentPane.add(projects, BorderLayout.CENTER); 262 | ThumbnailGuiHacker.this.revalidate(); 263 | ThumbnailGuiHacker.this.repaint(); 264 | } 265 | }); 266 | setLoadingState(true); 267 | try { 268 | JSONArray projects = GuiHackerBackend.getProjects(); 269 | for (int i = 0; i < projects.length(); i++) { 270 | JSONObject obj = projects.getJSONObject(i); 271 | int id = obj.getInt("pk"); 272 | String title = obj.getJSONObject("fields").getString("title"); 273 | this.projects.listModel.addElement(new Project(title, id)); 274 | } 275 | this.projects.list.setEnabled(true); 276 | this.projects.list.setModel(this.projects.listModel); 277 | this.projects.btnChangeIt.setEnabled(true); 278 | this.projects.revalidate(); 279 | } catch (Exception e) { 280 | e.printStackTrace(); 281 | setLoadingState(false); 282 | showExceptionDialog(e); 283 | } finally { 284 | setLoadingState(false); 285 | } 286 | } 287 | 288 | public void goToFileChooser(Project selected) { 289 | setTitle("Thumbnail Hacker - Choose a new thumbnail:"); 290 | selectedProject = selected; 291 | SwingUtilities.invokeLater(new Runnable() { 292 | @Override 293 | public void run() { 294 | contentPane.remove(projects); 295 | contentPane.add(chooser, BorderLayout.CENTER); 296 | ThumbnailGuiHacker.this.revalidate(); 297 | } 298 | }); 299 | } 300 | 301 | public void executeHack(final File selected, final String mimeType) { 302 | setLoadingState(true); 303 | new Thread(new Runnable() { 304 | @Override 305 | public void run() { 306 | try { 307 | loadingP.setIndeterminate(false); 308 | GuiHackerBackend.hackThumbnail(selectedProject.id, 309 | selected, mimeType); 310 | loadingP.setIndeterminate(true); 311 | loading.setVisible(false); 312 | success(); 313 | } catch (Exception e) { 314 | e.printStackTrace(); 315 | setLoadingState(false); 316 | showExceptionDialog(e); 317 | } finally { 318 | setLoadingState(false); 319 | } 320 | } 321 | }).start(); 322 | } 323 | 324 | public void setProgress(final int progress) { 325 | try { 326 | SwingUtilities.invokeAndWait(new Runnable() { 327 | @Override 328 | public void run() { 329 | loadingP.setValue(progress); 330 | } 331 | }); 332 | } catch (InvocationTargetException e) { 333 | e.printStackTrace(); 334 | } catch (InterruptedException e) { 335 | e.printStackTrace(); 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/apu/scratch/hax/GuiHackerFileChooser.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package apu.scratch.hax; 5 | 6 | import java.awt.BorderLayout; 7 | import java.awt.Component; 8 | import java.awt.Container; 9 | import java.awt.event.ActionEvent; 10 | import java.awt.event.ActionListener; 11 | import java.awt.image.BufferedImage; 12 | import java.awt.image.ColorModel; 13 | import java.awt.image.RenderedImage; 14 | import java.awt.image.WritableRaster; 15 | import java.io.File; 16 | import java.io.FileOutputStream; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.nio.file.Files; 20 | import java.nio.file.Paths; 21 | import java.util.ArrayList; 22 | import java.util.Hashtable; 23 | import java.util.Iterator; 24 | import java.util.List; 25 | 26 | import javax.imageio.IIOImage; 27 | import javax.imageio.ImageIO; 28 | import javax.imageio.ImageReader; 29 | import javax.imageio.ImageWriter; 30 | import javax.imageio.metadata.IIOMetadataNode; 31 | import javax.imageio.stream.FileImageInputStream; 32 | import javax.imageio.stream.FileImageOutputStream; 33 | import javax.imageio.stream.ImageInputStream; 34 | import javax.imageio.stream.ImageOutputStream; 35 | import javax.swing.JButton; 36 | import javax.swing.JFileChooser; 37 | import javax.swing.JPanel; 38 | import javax.swing.SwingUtilities; 39 | import javax.swing.filechooser.FileFilter; 40 | 41 | import apu.scratch.hax.GuiHackerFileChooser.GuiHackerFileFilter.FileFilterType; 42 | 43 | /** 44 | * File chooser 45 | * 46 | * @author "MegaApuTurkUltra" 47 | */ 48 | public class GuiHackerFileChooser extends JPanel { 49 | private static final long serialVersionUID = 8323299434700855672L; 50 | JFileChooser fileChooser; 51 | 52 | static class GuiHackerFileFilter extends FileFilter { 53 | static enum FileFilterType { 54 | GIF(new String[] { "gif" }, "image/gif", "GIF Images"), PNG( 55 | new String[] { "png" }, "image/png", "PNG Images"), JPEG( 56 | new String[] { "jpeg", "jpg" }, "image/jpeg", "JPEG Images"), OTHER( 57 | null, "application/octet-stream", "Other files"); 58 | String mime; 59 | String[] ext; 60 | String desc; 61 | 62 | private FileFilterType(String[] e, String m, String d) { 63 | ext = e; 64 | mime = m; 65 | desc = d; 66 | } 67 | 68 | public String getDesc() { 69 | return desc; 70 | } 71 | 72 | public String getMimeType() { 73 | return mime; 74 | } 75 | 76 | public boolean isFileAccepted(File f) { 77 | if (ext == null) 78 | return !f.isDirectory() && f.exists(); 79 | for (String s : ext) { 80 | if (f.getName().endsWith(s) && f.exists()) 81 | return true; 82 | } 83 | return false; 84 | } 85 | } 86 | 87 | FileFilterType type; 88 | 89 | public GuiHackerFileFilter(FileFilterType t) { 90 | type = t; 91 | } 92 | 93 | @Override 94 | public boolean accept(File f) { 95 | return type.isFileAccepted(f) || f.isDirectory(); 96 | } 97 | 98 | @Override 99 | public String getDescription() { 100 | return type.getDesc(); 101 | } 102 | } 103 | 104 | /** 105 | * Create the panel. 106 | */ 107 | public GuiHackerFileChooser() { 108 | fileChooser = new JFileChooser(); 109 | fileChooser.setMultiSelectionEnabled(false); 110 | fileChooser.setFileHidingEnabled(false); 111 | fileChooser.setAcceptAllFileFilterUsed(false); 112 | fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); 113 | for (FileFilterType type : FileFilterType.values()) { 114 | fileChooser.addChoosableFileFilter(new GuiHackerFileFilter(type)); 115 | } 116 | fileChooser.setApproveButtonText("Hack it!"); 117 | setLayout(new BorderLayout(0, 0)); 118 | add(fileChooser); 119 | SwingUtilities.invokeLater(new Runnable() { 120 | @Override 121 | public void run() { 122 | try { 123 | removeCancelButton(fileChooser); 124 | } catch (Exception e) { 125 | e.printStackTrace(); 126 | // continue and ignore. The cancel button doesn't do 127 | // anything anyway 128 | } 129 | } 130 | }); 131 | 132 | fileChooser.addActionListener(new ActionListener() { 133 | @Override 134 | public void actionPerformed(ActionEvent e) { 135 | SwingUtilities.invokeLater(new Runnable() { 136 | @Override 137 | public void run() { 138 | FileFilterType type = ((GuiHackerFileFilter) fileChooser 139 | .getFileFilter()).type; 140 | File f = fileChooser.getSelectedFile(); 141 | if (f == null || f.isDirectory() || !f.exists()) 142 | return; 143 | if (type == FileFilterType.GIF) { 144 | if (!checkGifFrameRate(f)) 145 | return; 146 | 147 | // try { 148 | // f = watermark(f); 149 | // } catch (Exception e) { 150 | // e.printStackTrace(); 151 | // ThumbnailGuiHacker.INSTANCE.showExceptionDialog(e); 152 | // } 153 | } 154 | 155 | ThumbnailGuiHacker.INSTANCE.executeHack(f, type.mime); 156 | } 157 | }); 158 | } 159 | }); 160 | } 161 | 162 | public static void removeCancelButton(Container c) { 163 | for (int i = 0; i < c.getComponentCount(); i++) { 164 | Component comp = c.getComponent(i); 165 | 166 | if (comp instanceof JButton) { 167 | JButton b = (JButton) comp; 168 | 169 | if (b != null && b.getText() != null 170 | && b.getText().equals("Cancel")) { 171 | c.remove(b); 172 | } 173 | 174 | } else if (comp instanceof Container) { 175 | removeCancelButton((Container) comp); 176 | } 177 | } 178 | } 179 | 180 | public boolean checkGifFrameRate(File f) { 181 | try { 182 | FileImageInputStream in = new FileImageInputStream(f); 183 | List delays = getGifDelays(in); 184 | in.close(); 185 | boolean shortDelay = false; 186 | for (Integer x : delays) { 187 | if (x.intValue() < 50) { 188 | shortDelay = true; 189 | break; 190 | } 191 | } 192 | if (shortDelay) { 193 | return ThumbnailGuiHacker.INSTANCE.warnAnnoyingGif(); 194 | } 195 | } catch (Exception e) { 196 | e.printStackTrace(); 197 | ThumbnailGuiHacker.INSTANCE.showExceptionDialog(e); 198 | } 199 | return true; 200 | } 201 | 202 | public static void main(String[] args) throws Exception { 203 | File test = new File("sample path"); 204 | File wm = watermark(test); 205 | Files.copy(Paths.get(wm.toURI()), new FileOutputStream("test.gif")); 206 | } 207 | 208 | /* 209 | * java, y u make dis so complicated? 210 | */ 211 | public static File watermark(File gif) throws Exception { 212 | ImageInputStream iin = new FileImageInputStream(gif); 213 | File tmp = File.createTempFile("thumb_", ".gif"); 214 | tmp.deleteOnExit(); 215 | ImageOutputStream iout = new FileImageOutputStream(tmp); 216 | 217 | InputStream wmarkin = ThumbnailGuiHacker.class 218 | .getResourceAsStream("/apu/scratch/hax/watermark.png"); 219 | BufferedImage wmark = ImageIO.read(wmarkin); 220 | wmarkin.close(); 221 | 222 | Iterator readers = ImageIO.getImageReaders(iin); 223 | 224 | ImageReader reader = null; 225 | while (readers.hasNext()) { 226 | reader = readers.next(); 227 | 228 | String metaFormat = reader.getOriginatingProvider() 229 | .getNativeImageMetadataFormatName(); 230 | if ("gif".equalsIgnoreCase(reader.getFormatName()) 231 | && !"javax_imageio_gif_image_1.0".equals(metaFormat)) { 232 | continue; 233 | } else { 234 | break; 235 | } 236 | } 237 | if (reader == null) { 238 | iout.close(); 239 | throw new IOException("Can not read image format!"); 240 | } 241 | 242 | ImageWriter writer = ImageIO.getImageWriter(reader); 243 | writer.setOutput(iout); 244 | 245 | boolean isGif = reader.getFormatName().equalsIgnoreCase("gif"); 246 | reader.setInput(iin, false, !isGif); 247 | writer.prepareWriteSequence(reader.getStreamMetadata()); 248 | for (int index = 0;; index++) { 249 | try { 250 | // read a frame and its metadata 251 | IIOImage frame = reader.readAll(index, null); 252 | RenderedImage img = frame.getRenderedImage(); 253 | if (img == null) 254 | throw new IllegalStateException("img == null"); 255 | BufferedImage newImg = convertRenderedImage(img); 256 | newImg.getGraphics().drawImage(wmark, 0, 0, newImg.getWidth(), 257 | newImg.getHeight(), 0, 0, wmark.getWidth(), 258 | wmark.getHeight(), null); 259 | frame.setRenderedImage(newImg); 260 | writer.writeToSequence(frame, null); 261 | } catch (IndexOutOfBoundsException e) { 262 | break; 263 | } 264 | } 265 | 266 | // clean up 267 | reader.dispose(); 268 | iout.close(); 269 | writer.dispose(); 270 | Files.copy(Paths.get(tmp.toURI()), new FileOutputStream("test.gif")); 271 | return tmp; 272 | } 273 | 274 | public static BufferedImage convertRenderedImage(RenderedImage img) { 275 | if (img instanceof BufferedImage) { 276 | return (BufferedImage) img; 277 | } 278 | ColorModel cm = img.getColorModel(); 279 | int width = img.getWidth(); 280 | int height = img.getHeight(); 281 | WritableRaster raster = cm 282 | .createCompatibleWritableRaster(width, height); 283 | boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); 284 | Hashtable properties = new Hashtable<>(); 285 | String[] keys = img.getPropertyNames(); 286 | if (keys != null) { 287 | for (int i = 0; i < keys.length; i++) { 288 | properties.put(keys[i], img.getProperty(keys[i])); 289 | } 290 | } 291 | BufferedImage result = new BufferedImage(cm, raster, 292 | isAlphaPremultiplied, properties); 293 | img.copyData(raster); 294 | return result; 295 | } 296 | 297 | private List getGifDelays(ImageInputStream imageStream) 298 | throws IOException { 299 | 300 | // obtain an appropriate src reader 301 | Iterator readers = ImageIO.getImageReaders(imageStream); 302 | 303 | ImageReader reader = null; 304 | while (readers.hasNext()) { 305 | reader = readers.next(); 306 | 307 | String metaFormat = reader.getOriginatingProvider() 308 | .getNativeImageMetadataFormatName(); 309 | if ("gif".equalsIgnoreCase(reader.getFormatName()) 310 | && !"javax_imageio_gif_image_1.0".equals(metaFormat)) { 311 | continue; 312 | } else { 313 | break; 314 | } 315 | } 316 | if (reader == null) { 317 | throw new IOException("Can not read image format!"); 318 | } 319 | boolean isGif = reader.getFormatName().equalsIgnoreCase("gif"); 320 | reader.setInput(imageStream, false, !isGif); 321 | List delays = new ArrayList(); 322 | boolean unkownMetaFormat = false; 323 | for (int index = 0;; index++) { 324 | try { 325 | // read a frame and its metadata 326 | IIOImage frame = reader.readAll(index, null); 327 | 328 | if (unkownMetaFormat) 329 | continue; 330 | 331 | // obtain src metadata 332 | javax.imageio.metadata.IIOMetadata meta = frame.getMetadata(); 333 | 334 | IIOMetadataNode imgRootNode = null; 335 | try { 336 | imgRootNode = (IIOMetadataNode) meta 337 | .getAsTree("javax_imageio_gif_image_1.0"); 338 | } catch (IllegalArgumentException e) { 339 | // unkown metadata format, can't do anyting about this 340 | unkownMetaFormat = true; 341 | continue; 342 | } 343 | 344 | IIOMetadataNode gce = (IIOMetadataNode) imgRootNode 345 | .getElementsByTagName("GraphicControlExtension") 346 | .item(0); 347 | 348 | delays.add(Integer.parseInt(gce.getAttribute("delayTime"))); 349 | } catch (IndexOutOfBoundsException e) { 350 | break; 351 | } 352 | } 353 | 354 | // clean up 355 | reader.dispose(); 356 | return delays; 357 | } 358 | } 359 | --------------------------------------------------------------------------------