Skip to content

Commit

Permalink
thumbnail in pxc files
Browse files Browse the repository at this point in the history
  • Loading branch information
lbalazscs committed Dec 4, 2024
1 parent 7a87d58 commit 40b4711
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 38 deletions.
56 changes: 43 additions & 13 deletions src/main/java/pixelitor/gui/utils/ImagePreviewPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@

package pixelitor.gui.utils;

import pixelitor.io.FileUtils;
import pixelitor.io.TrackedIO;
import pixelitor.io.*;
import pixelitor.utils.JProgressBarTracker;
import pixelitor.utils.ProgressPanel;
import pixelitor.utils.ProgressTracker;
Expand All @@ -27,9 +26,11 @@
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.nio.file.Files;
import java.util.HashMap;
Expand Down Expand Up @@ -80,28 +81,57 @@ private ThumbInfo getOrCreateThumb(File file) {
}
}

ThumbInfo readResult = readThumb(file);
if (readResult.isSuccess()) {
// don't cache failures - perhaps the user
// is retrying after fixing the problem
thumbsCache.put(filePath, new SoftReference<>(readResult));
}
return readResult;
}

private ThumbInfo readThumb(File file) {
if (!Files.isReadable(file.toPath())) {
return ThumbInfo.failure(ThumbInfo.PREVIEW_ERROR);
}

// Currently, no thumb extraction is attempted for ora and pxc files.
if (FileUtils.hasMultiLayerExtension(file)) {
ThumbInfo fakeThumbInfo = ThumbInfo.failure(ThumbInfo.NO_PREVIEW);
thumbsCache.put(filePath, new SoftReference<>(fakeThumbInfo));
return fakeThumbInfo;
String extension = FileUtils.getExtension(file.getName());

if ("pxc".equalsIgnoreCase(extension)) {
try {
BufferedImage thumbnail = PXCFormat.readThumbnail(file);
if (thumbnail == null) {
// old pxc file, without thumbnail
return ThumbInfo.failure(ThumbInfo.NO_PREVIEW);
}
return ThumbInfo.success(thumbnail);
} catch (BadPxcFormatException e) {
// not in pxc format
return ThumbInfo.failure(ThumbInfo.PREVIEW_ERROR);
}
}

if ("ora".equalsIgnoreCase(extension)) {
try {
BufferedImage thumbnail = OpenRaster.readThumbnail(file);
if (thumbnail == null) {
// old ora file, without thumbnail
return ThumbInfo.failure(ThumbInfo.NO_PREVIEW);
}
return ThumbInfo.success(thumbnail);
} catch (IOException e) {
// not zip format
return ThumbInfo.failure(ThumbInfo.PREVIEW_ERROR);
}
}

int availableWidth = getWidth() - EMPTY_SPACE_AT_LEFT;
int availableHeight = getHeight();
try {
ProgressTracker pt = new JProgressBarTracker(progressPanel);
ThumbInfo newThumbInfo = TrackedIO.readThumbnail(file, availableWidth, availableHeight, pt);
thumbsCache.put(filePath, new SoftReference<>(newThumbInfo));
return newThumbInfo;
return TrackedIO.readThumbnail(file, availableWidth, availableHeight, pt);
} catch (Exception ex) {
ThumbInfo fakeThumbInfo = ThumbInfo.failure(ThumbInfo.PREVIEW_ERROR);
thumbsCache.put(filePath, new SoftReference<>(fakeThumbInfo));
return fakeThumbInfo;
return ThumbInfo.failure(ThumbInfo.PREVIEW_ERROR);
}
}

Expand Down
11 changes: 10 additions & 1 deletion src/main/java/pixelitor/gui/utils/ThumbInfo.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Laszlo Balazs-Csiki and Contributors
* Copyright 2024 Laszlo Balazs-Csiki and Contributors
*
* This file is part of Pixelitor. Pixelitor is free software: you
* can redistribute it and/or modify it under the terms of the GNU
Expand Down Expand Up @@ -56,6 +56,11 @@ public static ThumbInfo success(BufferedImage thumb, int origWidth, int origHeig
return new ThumbInfo(thumb, origWidth, origHeight, null);
}

// success, but no original size info
public static ThumbInfo success(BufferedImage thumb) {
return new ThumbInfo(thumb, -1, -1, null);
}

public static ThumbInfo failure(int origWidth, int origHeight, String errMsg) {
return new ThumbInfo(null, origWidth, origHeight, errMsg);
}
Expand Down Expand Up @@ -103,4 +108,8 @@ private void paintImageSize(Graphics2D g, JPanel panel) {
g.setColor(WHITE);
g.drawString(msg, drawX - 1, drawY - 1);
}

public boolean isSuccess() {
return errMsg != null;
}
}
19 changes: 18 additions & 1 deletion src/main/java/pixelitor/io/OpenRaster.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import pixelitor.layers.*;
import pixelitor.utils.*;

import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.Dimension;
Expand Down Expand Up @@ -339,7 +340,7 @@ private static Document loadXMLFromString(String xml)
return builder.parse(inputSource);
}

private static BufferedImage createORAThumbnail(BufferedImage src) {
public static BufferedImage createORAThumbnail(BufferedImage src) {
// Create a thumbnail according to the OpenRaster spec:
// "It must be a non-interlaced PNG with 8 bits per channel
// of at most 256x256 pixels. It should be as big as possible
Expand All @@ -348,4 +349,20 @@ private static BufferedImage createORAThumbnail(BufferedImage src) {
src.getWidth(), src.getHeight(), THUMBNAIL_MAX_DIMENSION, false);
return ImageUtils.resize(src, thumbSize.width, thumbSize.height);
}

/**
* Reads only the thumbnail from an OpenRaster file.
*/
public static BufferedImage readThumbnail(File file) throws IOException {
try (ZipFile zipFile = new ZipFile(file)) {
ZipEntry thumbnailEntry = zipFile.getEntry(THUMBNAIL_PATH);
if (thumbnailEntry == null) {
return null; // thumbnail not found
}

try (InputStream inputStream = zipFile.getInputStream(thumbnailEntry)) {
return ImageIO.read(inputStream);
}
}
}
}
120 changes: 103 additions & 17 deletions src/main/java/pixelitor/io/PXCFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import static java.awt.image.BufferedImage.TYPE_BYTE_GRAY;
import static pixelitor.utils.ImageUtils.getPixelArray;
Expand All @@ -37,7 +36,10 @@
* PXC file format support.
*/
public class PXCFormat {
private static final int CURRENT_PXC_VERSION_NUMBER = 0x03;
private static final int CURRENT_PXC_VERSION_NUMBER = 0x04;

// the first version supporting a thumbnail
private static final int THUMBNAIL_FORMAT_VERSION = 0x04;

// tracks the writing of the whole file
private static ProgressTracker mainPT;
Expand All @@ -48,8 +50,14 @@ private PXCFormat() {
}

public static Composition read(File file) throws BadPxcFormatException {
ProgressTracker tracker = new StatusBarProgressTracker(
"Reading " + file.getName(), (int) file.length());
return read(file, tracker);
}

public static Composition read(File file, ProgressTracker tracker) throws BadPxcFormatException {
Composition comp = null;
try (InputStream is = new ProgressTrackingInputStream(file)) {
try (InputStream is = new ProgressTrackingInputStream(new FileInputStream(file), tracker)) {
int firstByte = is.read();
int secondByte = is.read();
if (firstByte == 0xAB && secondByte == 0xC4) {
Expand Down Expand Up @@ -79,16 +87,29 @@ public static Composition read(File file) throws BadPxcFormatException {
+ " has unknown version byte " + versionByte);
}

try (GZIPInputStream gs = new GZIPInputStream(is)) {
try (ObjectInput ois = new ObjectInputStream(gs)) {
comp = (Composition) ois.readObject();

// file is transient in Composition because the pxc file can be renamed
comp.setFile(file);
// Skip thumbnail data
if (versionByte >= THUMBNAIL_FORMAT_VERSION) {
// Read thumbnail length (4 bytes)
int thumbnailLength = readInt(is);
// Skip the thumbnail data
is.skip(thumbnailLength);
}

EventQueue.invokeLater(comp::checkFontsAreInstalled);
if (versionByte == 3) { // gzipped stream in old pxc files
try (GZIPInputStream gs = new GZIPInputStream(is)) {
try (ObjectInput ois = new ObjectInputStream(gs)) {
comp = (Composition) ois.readObject();
}
}
} else {
try (ObjectInput ois = new ObjectInputStream(is)) {
comp = (Composition) ois.readObject();
}
}
// file is transient in Composition because the pxc file can be renamed
comp.setFile(file);

EventQueue.invokeLater(comp::checkFontsAreInstalled);
} catch (IOException | ClassNotFoundException e) {
Messages.showException(e);
}
Expand All @@ -106,13 +127,29 @@ public static void write(Composition comp, File file) {
workRatioForOneImage = -1;
}
try (FileOutputStream fos = new FileOutputStream(file)) {
// write header bytes and version
fos.write(new byte[]{(byte) 0xAB, (byte) 0xC4, CURRENT_PXC_VERSION_NUMBER});

try (GZIPOutputStream gz = new GZIPOutputStream(fos)) {
try (ObjectOutput oos = new ObjectOutputStream(gz)) {
oos.writeObject(comp);
oos.flush();
}
// write thumbnail
BufferedImage thumbnail = OpenRaster.createORAThumbnail(comp.getCompositeImage());
// create an extra stream so that we know the length of the thumbnail data
ByteArrayOutputStream thumbnailBytes = new ByteArrayOutputStream();
ImageIO.write(thumbnail, "PNG", thumbnailBytes);
byte[] thumbnailData = thumbnailBytes.toByteArray();
writeInt(fos, thumbnailData.length); // write thumbnail length
fos.write(thumbnailData); // write thumbnail data

// try (GZIPOutputStream gz = new GZIPOutputStream(fos)) {
// try (ObjectOutput oos = new ObjectOutputStream(gz)) {
// oos.writeObject(comp);
// oos.flush();
// }
// }

// since pxc version 4, the stream isn't gzipped
try (ObjectOutput oos = new ObjectOutputStream(fos)) {
oos.writeObject(comp);
oos.flush();
}
} catch (IOException e) {
throw new UncheckedIOException(e);
Expand All @@ -121,22 +158,56 @@ public static void write(Composition comp, File file) {
mainPT = null;
}

/**
* Reads only the thumbnail from a PXC file.
*/
public static BufferedImage readThumbnail(File file) throws BadPxcFormatException {
try (InputStream is = new FileInputStream(file)) {
int firstByte = is.read();
int secondByte = is.read();
if (firstByte != 0xAB || secondByte != 0xC4) {
throw new BadPxcFormatException(file.getName() + " is not in the pxc format.");
}

int versionByte = is.read();
if (versionByte != THUMBNAIL_FORMAT_VERSION) {
return null; // old version, no thumbnail
}

int thumbnailLength = readInt(is);
byte[] thumbnailData = new byte[thumbnailLength];
is.read(thumbnailData);

try (ByteArrayInputStream bais = new ByteArrayInputStream(thumbnailData)) {
return ImageIO.read(bais);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

public static void serializeImage(ObjectOutputStream out,
BufferedImage img) throws IOException {
assert img != null;
int imgType = img.getType();
int imgWidth = img.getWidth();
int imgHeight = img.getHeight();

// in PXC version 3, only grayscale images were written
// as PNG, and for simplicity, we still write this field
// int imgType = img.getType();
int imgType = TYPE_BYTE_GRAY;

out.writeInt(imgWidth);
out.writeInt(imgHeight);
out.writeInt(imgType);

ProgressTracker pt = getImageTracker();

if (imgType == TYPE_BYTE_GRAY) {
ImageIO.write(img, "PNG", out);
TrackedIO.writeToStream(img, out, "PNG", pt);
// ImageIO.write(img, "PNG", out);
} else {
// this legacy branch is never executed anymore
int[] pixels = getPixelArray(img);
int length = pixels.length;
int progress = 0;
Expand All @@ -162,6 +233,7 @@ public static BufferedImage deserializeImage(ObjectInputStream in) throws IOExce
if (type == TYPE_BYTE_GRAY) {
return ImageIO.read(in);
} else {
// this branch is executed only for legacy (version 3) pxc files
BufferedImage img = new BufferedImage(width, height, type);
int[] pixels = getPixelArray(img);

Expand All @@ -181,4 +253,18 @@ private static ProgressTracker getImageTracker() {
return new SubtaskProgressTracker(workRatioForOneImage, mainPT);
}
}

// Reads 4 bytes as an int
private static int readInt(InputStream is) throws IOException {
return is.read() << 24 | (is.read() & 0xFF) << 16 |
(is.read() & 0xFF) << 8 | (is.read() & 0xFF);
}

// Writes an int as 4 bytes
private static void writeInt(OutputStream os, int value) throws IOException {
os.write((value >>> 24) & 0xFF);
os.write((value >>> 16) & 0xFF);
os.write((value >>> 8) & 0xFF);
os.write(value & 0xFF);
}
}
13 changes: 7 additions & 6 deletions src/main/java/pixelitor/io/ProgressTrackingInputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
package pixelitor.io;

import pixelitor.utils.ProgressTracker;
import pixelitor.utils.StatusBarProgressTracker;

import java.io.*;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilterInputStream;
import java.io.IOException;

/**
* A wrapper for an InputStream that tracks the progress of reading.
Expand All @@ -31,11 +33,10 @@ public class ProgressTrackingInputStream extends FilterInputStream {
private final ProgressTracker progressTracker;
private boolean closed = false;

public ProgressTrackingInputStream(File file) throws FileNotFoundException {
super(new FileInputStream(file));
public ProgressTrackingInputStream(FileInputStream inputStream, ProgressTracker tracker) throws FileNotFoundException {
super(inputStream);

this.progressTracker = new StatusBarProgressTracker(
"Reading " + file.getName(), (int) file.length());
this.progressTracker = tracker;
}

@Override
Expand Down
16 changes: 16 additions & 0 deletions stuff/pxc/thumbnails/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CC = gcc
CFLAGS = -Wall -Wextra -pedantic -O2
LDFLAGS =

SRC = extract_pxc_thumbnails.c
TARGET = extract_pxc_thumbnails

all: $(TARGET)

$(TARGET): $(SRC)
$(CC) $(CFLAGS) $(SRC) -o $(TARGET) $(LDFLAGS)

clean:
rm -f $(TARGET)

.PHONY: all clean
Loading

0 comments on commit 40b4711

Please sign in to comment.