Last active
May 4, 2020 15:02
-
-
Save jsfan3/4d2d55f0d5136ec88d36257401d41854 to your computer and use it in GitHub Desktop.
Codename One fast image scaling algorithm to generate thumbnails from captured photos. This optimization makes sense on Android and Simulator (Java SE) only. See: https://github.com/codenameone/CodenameOne/issues/3090
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.informaticalibera.test.imagescaling; | |
import com.codename1.capture.Capture; | |
import com.codename1.components.SpanLabel; | |
import com.codename1.io.FileSystemStorage; | |
import static com.codename1.ui.CN.*; | |
import com.codename1.ui.Form; | |
import com.codename1.ui.Dialog; | |
import com.codename1.ui.Label; | |
import com.codename1.ui.plaf.UIManager; | |
import com.codename1.ui.util.Resources; | |
import com.codename1.io.Log; | |
import com.codename1.ui.Toolbar; | |
import java.io.IOException; | |
import com.codename1.ui.layouts.BoxLayout; | |
import com.codename1.io.Util; | |
import com.codename1.ui.Button; | |
import com.codename1.ui.CN; | |
import com.codename1.ui.EncodedImage; | |
import com.codename1.ui.Image; | |
import com.codename1.ui.events.ActionListener; | |
import com.codename1.ui.util.ImageIO; | |
import java.io.InputStream; | |
import java.io.OutputStream; | |
/** | |
* This file was generated by <a href="https://www.codenameone.com/">Codename | |
* One</a> for the purpose of building native mobile applications using Java. | |
*/ | |
public class MyApplication { | |
private Form current; | |
private Resources theme; | |
public void init(Object context) { | |
// use two network threads instead of one | |
updateNetworkThreadCount(2); | |
theme = UIManager.initFirstTheme("/theme"); | |
// Enable Toolbar on all Forms by default | |
Toolbar.setGlobalToolbar(true); | |
// Pro only feature | |
Log.bindCrashProtection(true); | |
addNetworkErrorListener(err -> { | |
// prevent the event from propagating | |
err.consume(); | |
if (err.getError() != null) { | |
Log.e(err.getError()); | |
} | |
Log.sendLogAsync(); | |
Dialog.show("Connection Error", "There was a networking error in the connection to " + err.getConnectionRequest().getUrl(), "OK", null); | |
}); | |
} | |
public void start() { | |
if (current != null) { | |
current.show(); | |
return; | |
} | |
Form hi = new Form("Hi World", BoxLayout.y()); | |
Button button1 = new Button("Open camera"); | |
Button button2 = new Button("Open gallery"); | |
SpanLabel summary = new SpanLabel(); | |
Label label1 = new Label(); | |
Label label2 = new Label(); | |
Label label3 = new Label(); | |
Label label4 = new Label(); | |
Label label5 = new Label(); | |
Label label6 = new Label(); | |
hi.addAll(button1, button2, summary, label1, label2, label3, label4, label5, label6); | |
hi.show(); | |
ActionListener actionEvt = evt -> { | |
String tempPath = (String) evt.getSource(); | |
if (tempPath != null && FileSystemStorage.getInstance().exists(tempPath)) { | |
try { | |
// copy the temp file in the home | |
String capturedPhoto = FileSystemStorage.getInstance().getAppHomePath() + "captured.jpg"; | |
InputStream temp = FileSystemStorage.getInstance().openInputStream(tempPath); | |
OutputStream dest = FileSystemStorage.getInstance().openOutputStream(capturedPhoto); | |
Util.copy(temp, dest); | |
InputStream source = FileSystemStorage.getInstance().openInputStream(capturedPhoto); | |
Image original = EncodedImage.create(source); | |
int newWidth = CN.convertToPixels(30); | |
int newHeight = newWidth * original.getHeight() / original.getWidth(); | |
// resizes using the fastScaleImage method | |
long startTime1 = System.currentTimeMillis(); | |
Image thumbnail1 = fastScaleImage(original, newWidth, newHeight); | |
long endTime1 = System.currentTimeMillis(); | |
long time1 = endTime1 - startTime1; | |
label1.setIcon(thumbnail1); | |
// resizes using the Image.scale method | |
long startTime2 = System.currentTimeMillis(); | |
Image thumbnail2 = original.scaled(newWidth, newHeight); | |
long endTime2 = System.currentTimeMillis(); | |
long time2 = endTime2 - startTime2; | |
label2.setIcon(thumbnail2); | |
// resizes using the ImageIO.save method | |
long startTime3 = System.currentTimeMillis(); | |
String thumbPath = FileSystemStorage.getInstance().getAppHomePath() + "thumb.jpg"; | |
InputStream source3 = FileSystemStorage.getInstance().openInputStream(capturedPhoto); | |
OutputStream response = FileSystemStorage.getInstance().openOutputStream(thumbPath); | |
ImageIO.getImageIO().save(source3, response, ImageIO.FORMAT_JPEG, newWidth, newHeight, 0.9f); | |
long endTime3 = System.currentTimeMillis(); | |
InputStream thumbStream = FileSystemStorage.getInstance().openInputStream(thumbPath); | |
Image thumbnail = EncodedImage.create(thumbStream); | |
long time3 = endTime3 - startTime3; | |
label3.setIcon(thumbnail); | |
// Test of make an image bigger | |
// resizes using the fastScaleImage method | |
long startTime4 = System.currentTimeMillis(); | |
Image thumbnail4 = fastScaleImage(thumbnail, original.getWidth(), original.getHeight()); | |
long endTime4 = System.currentTimeMillis(); | |
long time4 = endTime4 - startTime4; | |
label4.setIcon(thumbnail4); | |
// resizes using the Image.scale method | |
long startTime5 = System.currentTimeMillis(); | |
Image thumbnail5 = thumbnail.scaled(original.getWidth(), original.getHeight()); | |
long endTime5 = System.currentTimeMillis(); | |
long time5 = endTime5 - startTime5; | |
label5.setIcon(thumbnail5); | |
// resizes using the ImageIO.save method | |
long startTime6 = System.currentTimeMillis(); | |
String newImgPath = FileSystemStorage.getInstance().getAppHomePath() + "newImg.jpg"; | |
ImageIO.getImageIO().saveAndKeepAspect(thumbPath, newImgPath, ImageIO.FORMAT_JPEG, original.getWidth(), original.getHeight(), 0.9f, false, true); | |
long endTime6 = System.currentTimeMillis(); | |
InputStream newImgStream = FileSystemStorage.getInstance().openInputStream(newImgPath); | |
Image thumbnail6 = EncodedImage.create(newImgStream); | |
long time6 = endTime6 - startTime6; | |
label6.setIcon(thumbnail6); | |
summary.setText("RESIZING TO A SMALLER IMAGE\n\nfastScaleImage time: " + time1 + "\n" + "Image.scaled() time: " + time2 + "\n" + "ImageIO.save() time: " + time3 + "\n\n" | |
+ "RESIZING TO A BIGGER IMAGE\n\nfastScaleImage time: " + time4 + "\n" + "Image.scaled() time: " + time5 + "\n" + "ImageIO.save() time: " + time6); | |
hi.revalidate(); | |
} catch (IOException ex) { | |
Log.p("Error loading the photo", Log.ERROR); | |
Log.e(ex); | |
} | |
} | |
}; | |
button1.addActionListener(l -> { | |
Capture.capturePhoto(actionEvt); | |
}); | |
button2.addActionListener(l -> { | |
CN.openGallery(actionEvt, CN.GALLERY_IMAGE); | |
}); | |
} | |
public void stop() { | |
current = getCurrentForm(); | |
if (current instanceof Dialog) { | |
((Dialog) current).dispose(); | |
current = getCurrentForm(); | |
} | |
} | |
public void destroy() { | |
} | |
/** | |
* <p> | |
* Fast scaling algorithm, optimized to resize a captured photo to a | |
* thumbnail: in this use case, it's about 3x ~ 10x faster that | |
* Image.scaled() and IOImage.save() ON ANDROID AND SIMULATOR ONLY, with an | |
* equivalent quality. On iOS, the fastest algorithm is IOImage.save()</p> | |
* <p> | |
* CAUTION: Don't use to enlarge an image, because it will be slower than | |
* other methods.</p> | |
* <p> | |
* Speed comparation, see | |
* <a href="https://gist.github.com/jsfan3/4d2d55f0d5136ec88d36257401d41854">testing | |
* code</a></p> | |
* | |
* | |
* @param original Image | |
* @param newWidth in pixels | |
* @param newHeight in pixels | |
* @return new scaled Image (it will be an EncodedImage instance) | |
*/ | |
public static Image fastScaleImage(Image original, int newWidth, int newHeight) { | |
int[] rawInput = original.getRGB(); // ARGB array | |
int[] rawOutput = new int[newWidth * newHeight]; | |
int originalW = original.getWidth(); | |
int originalH = original.getHeight(); | |
int YD = (originalH / newHeight) * originalW - originalW; | |
int YR = originalH % newHeight; | |
int XD = originalW / newWidth; | |
int XR = originalW % newWidth; | |
int outOffset = 0; | |
int inOffset = 0; | |
for (int y = newHeight, YE = 0; y > 0; y--) { | |
for (int x = newWidth, XE = 0; x > 0; x--) { | |
rawOutput[outOffset++] = rawInput[inOffset]; | |
inOffset += XD; | |
XE += XR; | |
if (XE >= newWidth) { | |
XE -= newWidth; | |
inOffset++; | |
} | |
} | |
inOffset += YD; | |
YE += YR; | |
if (YE >= newHeight) { | |
YE -= newHeight; | |
inOffset += originalW; | |
} | |
} | |
return EncodedImage.createFromRGB(rawOutput, newWidth, newHeight, true); | |
} | |
} |
Author
jsfan3
commented
May 3, 2020
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment