diff --git a/README.md b/README.md index 34ec1e0b..355b8920 100644 --- a/README.md +++ b/README.md @@ -97,24 +97,25 @@ APIs: ``` Bitmap ▓▓▓▓▓▓▓▓▓▓ Paint ▓▓▓▓▓▓▓▓▓▓ Canvas ▓▓▓▓▓▓▓▓░░ Path ▓▓▓▓▓▓▓▓▓▓ -Color ▓░░░░░░░░░ PathEffects ▓▓▓▓▓▓▓▓▓▓ -ColorFilter ▓▓▓▓▓▓▓▓▓▓ PathMeasure ▓▓▓▓▓▓▓▓▓▓ -ColorInfo ▓▓▓▓▓▓▓▓▓▓ PaintFilterCanvas ▓▓▓▓▓▓▓▓▓▓ -ColorSpace ▓▓▓▓░░░░░░ Picture ▓▓▓▓▓▓▓▓▓░ -Data ▓▓▓▓▓▓▓▓▓░ PictureRecorder ▓▓▓▓▓▓▓▓▓▓ -Drawable ▓▓▓▓▓▓▓▓░░ PixelRef ▓▓▓▓▓▓▓▓▓▓ -Flattenable ░░░░░░░░░░ Pixmap ░░░░░░░░░░ -Font ▓▓▓▓▓▓▓▓▓▓ Region ▓▓▓▓▓▓▓▓▓▓ -FontData ░░░░░░░░░░ ScalerContext ░░░░░░░░░░ -FontManager ▓▓▓▓▓▓▓▓▓░ Shader ▓▓▓▓▓▓▓▓▓▓ -FontStyle ▓▓▓▓▓▓▓▓▓▓ ShadowUtils ▓▓▓▓▓▓▓▓▓▓ -FontStyleSet ▓▓▓▓▓▓▓▓▓▓ Stream ░░░░░░░░░░ -Image ▓▓░░░░░░░░ String ▓░░░░░░░░░ -ImageFilters ▓▓▓▓▓▓▓▓▓▓ Surface ▓░░░░░░░░░ -ImageInfo ▓▓▓▓▓▓▓▓▓▓ TextBlob ▓▓▓▓▓▓▓▓▓▓ -MaskFilter ▓▓▓▓▓▓▓▓▓▓ TextBlobBuilder ▓▓▓▓▓▓▓▓▓▓ -Matrix33 ▓▓▓░░░░░░░ Typeface ▓▓▓▓▓▓▓▓░░ -Matrix44 ▓▓▓░░░░░░░ WStream ▓▓░░░░░░░░ +Codec ▓▓▓▓░░░░░░ PathEffects ▓▓▓▓▓▓▓▓▓▓ +Color ▓░░░░░░░░░ PathMeasure ▓▓▓▓▓▓▓▓▓▓ +ColorFilter ▓▓▓▓▓▓▓▓▓▓ PaintFilterCanvas ▓▓▓▓▓▓▓▓▓▓ +ColorInfo ▓▓▓▓▓▓▓▓▓▓ Picture ▓▓▓▓▓▓▓▓▓░ +ColorSpace ▓▓▓▓░░░░░░ PictureRecorder ▓▓▓▓▓▓▓▓▓▓ +Data ▓▓▓▓▓▓▓▓▓░ PixelRef ▓▓▓▓▓▓▓▓▓▓ +Drawable ▓▓▓▓▓▓▓▓░░ Pixmap ░░░░░░░░░░ +Flattenable ░░░░░░░░░░ Region ▓▓▓▓▓▓▓▓▓▓ +Font ▓▓▓▓▓▓▓▓▓▓ ScalerContext ░░░░░░░░░░ +FontData ░░░░░░░░░░ Shader ▓▓▓▓▓▓▓▓▓▓ +FontManager ▓▓▓▓▓▓▓▓▓░ ShadowUtils ▓▓▓▓▓▓▓▓▓▓ +FontStyle ▓▓▓▓▓▓▓▓▓▓ Stream ░░░░░░░░░░ +FontStyleSet ▓▓▓▓▓▓▓▓▓▓ String ▓░░░░░░░░░ +Image ▓▓░░░░░░░░ Surface ▓░░░░░░░░░ +ImageFilters ▓▓▓▓▓▓▓▓▓▓ TextBlob ▓▓▓▓▓▓▓▓▓▓ +ImageInfo ▓▓▓▓▓▓▓▓▓▓ TextBlobBuilder ▓▓▓▓▓▓▓▓▓▓ +MaskFilter ▓▓▓▓▓▓▓▓▓▓ Typeface ▓▓▓▓▓▓▓▓░░ +Matrix33 ▓▓▓░░░░░░░ WStream ▓▓░░░░░░░░ +Matrix44 ▓▓▓░░░░░░░ Shaper: Paragraph: @@ -134,7 +135,6 @@ SVG: SVGDOM ▓▓▓▓▓▓▓▓░░ SVGCanvas ▓▓▓▓▓▓▓▓▓▓ - ``` ## Using Skija diff --git a/examples/scenes/images/codecs/animated.gif b/examples/scenes/images/codecs/animated.gif new file mode 100644 index 00000000..12d27921 Binary files /dev/null and b/examples/scenes/images/codecs/animated.gif differ diff --git a/examples/scenes/images/codecs/animated.webp b/examples/scenes/images/codecs/animated.webp new file mode 100644 index 00000000..8ef091ef Binary files /dev/null and b/examples/scenes/images/codecs/animated.webp differ diff --git a/examples/scenes/images/codecs/dotpeek.ico b/examples/scenes/images/codecs/dotpeek.ico new file mode 100644 index 00000000..53fc6090 Binary files /dev/null and b/examples/scenes/images/codecs/dotpeek.ico differ diff --git a/examples/scenes/images/codecs/heic_loseless.heic b/examples/scenes/images/codecs/heic_loseless.heic deleted file mode 100644 index 3409d406..00000000 Binary files a/examples/scenes/images/codecs/heic_loseless.heic and /dev/null differ diff --git a/examples/scenes/images/codecs/heic_lossy.heic b/examples/scenes/images/codecs/heic_lossy.heic deleted file mode 100644 index 320957f7..00000000 Binary files a/examples/scenes/images/codecs/heic_lossy.heic and /dev/null differ diff --git a/examples/scenes/images/codecs/jpeg2000_loseless.jp2 b/examples/scenes/images/codecs/jpeg2000_loseless.jp2 deleted file mode 100644 index c6b7b486..00000000 Binary files a/examples/scenes/images/codecs/jpeg2000_loseless.jp2 and /dev/null differ diff --git a/examples/scenes/images/codecs/jpeg2000_lossy.jp2 b/examples/scenes/images/codecs/jpeg2000_lossy.jp2 deleted file mode 100644 index 1c706cad..00000000 Binary files a/examples/scenes/images/codecs/jpeg2000_lossy.jp2 and /dev/null differ diff --git a/examples/scenes/images/codecs/orient_bl.jpg b/examples/scenes/images/codecs/orient_bl.jpg new file mode 100644 index 00000000..6928bc8d Binary files /dev/null and b/examples/scenes/images/codecs/orient_bl.jpg differ diff --git a/examples/scenes/images/codecs/orient_br.jpg b/examples/scenes/images/codecs/orient_br.jpg new file mode 100644 index 00000000..b783b6b8 Binary files /dev/null and b/examples/scenes/images/codecs/orient_br.jpg differ diff --git a/examples/scenes/images/codecs/orient_lb.jpg b/examples/scenes/images/codecs/orient_lb.jpg new file mode 100644 index 00000000..e5d22424 Binary files /dev/null and b/examples/scenes/images/codecs/orient_lb.jpg differ diff --git a/examples/scenes/images/codecs/orient_lt.jpg b/examples/scenes/images/codecs/orient_lt.jpg new file mode 100644 index 00000000..81b224b3 Binary files /dev/null and b/examples/scenes/images/codecs/orient_lt.jpg differ diff --git a/examples/scenes/images/codecs/orient_rb.jpg b/examples/scenes/images/codecs/orient_rb.jpg new file mode 100644 index 00000000..ff09095e Binary files /dev/null and b/examples/scenes/images/codecs/orient_rb.jpg differ diff --git a/examples/scenes/images/codecs/orient_rt.jpg b/examples/scenes/images/codecs/orient_rt.jpg new file mode 100644 index 00000000..35eca2fe Binary files /dev/null and b/examples/scenes/images/codecs/orient_rt.jpg differ diff --git a/examples/scenes/images/codecs/orient_tl.jpg b/examples/scenes/images/codecs/orient_tl.jpg new file mode 100644 index 00000000..1aa26b43 Binary files /dev/null and b/examples/scenes/images/codecs/orient_tl.jpg differ diff --git a/examples/scenes/images/codecs/orient_tr.jpg b/examples/scenes/images/codecs/orient_tr.jpg new file mode 100644 index 00000000..15065b08 Binary files /dev/null and b/examples/scenes/images/codecs/orient_tr.jpg differ diff --git a/examples/scenes/images/codecs/tiff.tiff b/examples/scenes/images/codecs/tiff.tiff deleted file mode 100644 index 4c5ad670..00000000 Binary files a/examples/scenes/images/codecs/tiff.tiff and /dev/null differ diff --git a/examples/scenes/src/CodecScene.java b/examples/scenes/src/CodecScene.java new file mode 100644 index 00000000..bf76a03f --- /dev/null +++ b/examples/scenes/src/CodecScene.java @@ -0,0 +1,137 @@ +package org.jetbrains.skija.examples.scenes; + +import java.io.*; +import java.util.*; +import java.util.stream.*; +import org.jetbrains.skija.*; + +public class CodecScene extends Scene { + Paint stroke = new Paint().setColor(0x80CC3333).setMode(PaintMode.STROKE).setStrokeWidth(1); + List> formats = new ArrayList<>(); + List> orientations = new ArrayList<>(); + + static class Animation { + Codec codec; + Bitmap bitmap; + int prevFrame = -1; + int[] durations; + long totalDuration; + } + + List> animations = new ArrayList<>(); + + float x, y; + float rowH = 100; + float columnW = 100; + + public CodecScene() { + for (var file: new String[] {"bmp.bmp", "gif.gif", "favicon.ico", "dotpeek.ico", "jpeg.jpg", "png.png", "webp_lossy.webp", "webp_loseless.webp"}) { + try (var codec = Codec.makeFromData(Data.makeFromFileName(file("images/codecs/" + file)))) { + formats.add(new Pair(file + "\n" + codec.getEncodedImageFormat(), codec.readPixels())); + } catch (Exception e) { + formats.add(new Pair(file + "\n" + e.getMessage(), null)); + } + } + + for (var file: new String[] {"orient_tl.jpg", "orient_tr.jpg", "orient_br.jpg", "orient_bl.jpg", "orient_lt.jpg", "orient_lb.jpg", "orient_rb.jpg", "orient_rt.jpg",}) { + orientations.add(new Pair(file, Codec.makeFromData(Data.makeFromFileName(file("images/codecs/" + file))))); + } + + for (var file: new String[] {"animated.gif", "animated.webp"}) { + var animation = new Animation(); + animation.codec = Codec.makeFromData(Data.makeFromFileName(file("images/codecs/" + file))); + animation.bitmap = new Bitmap(); + animation.bitmap.allocPixels(animation.codec.getImageInfo()); + animation.durations = Arrays.stream(animation.codec.getFramesInfo()).mapToInt(AnimationFrameInfo::getDuration).toArray(); + animation.totalDuration = Arrays.stream(animation.durations).sum(); + animations.add(new Pair(file, animation)); + } + } + + public void drawOne(Canvas canvas, int width, String s, Runnable draw) { + if (x + columnW >= width) { + x = 20; + y += rowH + 60; + } + canvas.save(); + canvas.translate(x, y); + draw.run(); + + var lines = s.lines().toArray(); + for (int i = 0; i < lines.length; ++i) + canvas.drawString((String) lines[i], 0, rowH + 20 + i * 20, inter13, blackFill); + canvas.restore(); + x += columnW + 20; + } + + @Override + public void draw(Canvas canvas, int width, int height, float dpi, int xpos, int ypos) { + x = 20; + y = 20; + + for (var pair: formats) { + var label = pair.getFirst(); + var bitmap = pair.getSecond(); + if (bitmap == null) { + drawOne(canvas, width, label, () -> { + canvas.drawRect(Rect.makeXYWH(0, 0, columnW, rowH), stroke); + canvas.drawLine(0, 0, columnW, rowH, stroke); + canvas.drawLine(0, rowH, columnW, 0, stroke); + }); + } else { + drawOne(canvas, width, label, () -> { canvas.drawBitmapRect(bitmap, Rect.makeXYWH(0, 0, columnW, rowH)); }); + } + } + + x = 20; + y += rowH + 60; + for (var pair: orientations) { + var label = pair.getFirst(); + var codec = pair.getSecond(); + var origin = codec.getEncodedOrigin(); + try (var bitmap = codec.readPixels()) { + int bitmapWidth = origin.swapsWidthHeight() ? codec.getHeight() : codec.getWidth(); + int bitmapHeight = origin.swapsWidthHeight() ? codec.getWidth() : codec.getHeight(); + drawOne(canvas, width, label + "\n" + codec.getEncodedImageFormat(), () -> { + canvas.save(); + canvas.concat(origin.toMatrix(bitmapWidth, bitmapHeight)); + canvas.drawBitmapRect(bitmap, Rect.makeXYWH(0, 0, codec.getWidth(), codec.getHeight())); + canvas.restore(); + canvas.drawRect(Rect.makeXYWH(0, 0, bitmapWidth, bitmapHeight), stroke); + }); + } + } + + x = 20; + y += rowH + 60; + + for (var pair: animations) { + var label = pair.getFirst(); + var animation = pair.getSecond(); + var codec = animation.codec; + + int duration = 0; + int frame = 0; + long now = System.currentTimeMillis() % animation.totalDuration; + for (; frame < animation.durations.length; ++frame) { + duration += animation.durations[frame]; + if (duration >= now) + break; + } + int finalFrame = frame; + + drawOne(canvas, width, label + "\n" + codec.getEncodedImageFormat(), () -> { + try (var bitmap = new Bitmap()) { + bitmap.allocPixels(codec.getImageInfo()); + codec.readPixels(bitmap, finalFrame); + canvas.drawBitmapRect(bitmap, Rect.makeXYWH(0, 0, columnW, rowH)); + } + }); + + drawOne(canvas, width, label + "\n" + codec.getEncodedImageFormat() + " + priorFrame", () -> { + codec.readPixels(animation.bitmap, finalFrame, animation.prevFrame); + canvas.drawBitmapRect(animation.bitmap, Rect.makeXYWH(0, 0, columnW, rowH)); + }); + } + } +} \ No newline at end of file diff --git a/examples/scenes/src/ImageCodecsScene.java b/examples/scenes/src/ImageCodecsScene.java index 508fe8c9..815f1327 100644 --- a/examples/scenes/src/ImageCodecsScene.java +++ b/examples/scenes/src/ImageCodecsScene.java @@ -23,14 +23,10 @@ public void loadImage(String file) { public ImageCodecsScene() { loadImage("bmp.bmp"); loadImage("gif.gif"); - loadImage("heic_lossy.heic"); - loadImage("heic_loseless.heic"); loadImage("favicon.ico"); + loadImage("dotpeek.ico"); loadImage("jpeg.jpg"); - loadImage("jpeg2000_lossy.jp2"); - loadImage("jpeg2000_loseless.jp2"); loadImage("png.png"); - loadImage("tiff.tiff"); loadImage("webp_lossy.webp"); loadImage("webp_loseless.webp"); } diff --git a/examples/scenes/src/Scenes.java b/examples/scenes/src/Scenes.java index 24a6540a..f776be82 100644 --- a/examples/scenes/src/Scenes.java +++ b/examples/scenes/src/Scenes.java @@ -6,7 +6,7 @@ public class Scenes { public static TreeMap scenes; - public static String currentScene = "Image Codecs"; + public static String currentScene = "Codec"; public static HUD hud = new HUD(); public static boolean vsync = true; public static boolean stats = true; @@ -16,6 +16,7 @@ public class Scenes { scenes.put("Bitmap", null); scenes.put("Bitmap Image", null); scenes.put("Blends", null); + scenes.put("Codec", null); scenes.put("Color Filters", null); scenes.put("Decorations Bench", null); scenes.put("Drawable", null); diff --git a/native/src/Codec.cc b/native/src/Codec.cc new file mode 100644 index 00000000..693288b8 --- /dev/null +++ b/native/src/Codec.cc @@ -0,0 +1,90 @@ +#include +#include +#include "SkBitmap.h" +#include "SkCodec.h" +#include "SkData.h" +#include "interop.hh" + +static void deleteCodec(SkCodec* instance) { + delete instance; +} + +extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_Codec__1nGetFinalizer(JNIEnv* env, jclass jclass) { + return static_cast(reinterpret_cast(&deleteCodec)); +} + +extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_Codec__1nMakeFromData + (JNIEnv* env, jclass jclass, jlong dataPtr) { + SkData* data = reinterpret_cast(static_cast(dataPtr)); + std::unique_ptr instance = SkCodec::MakeFromData(sk_ref_sp(data)); + return reinterpret_cast(instance.release()); +} + +extern "C" JNIEXPORT jobject JNICALL Java_org_jetbrains_skija_Codec__1nGetImageInfo + (JNIEnv* env, jclass jclass, jlong ptr) { + SkCodec* instance = reinterpret_cast(static_cast(ptr)); + return skija::ImageInfo::toJava(env, instance->getInfo()); +} + +extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_Codec__1nGetSize + (JNIEnv* env, jclass jclass, jlong ptr) { + SkCodec* instance = reinterpret_cast(static_cast(ptr)); + return packISize(instance->dimensions()); +} + +extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_Codec__1nGetEncodedOrigin + (JNIEnv* env, jclass jclass, jlong ptr) { + SkCodec* instance = reinterpret_cast(static_cast(ptr)); + return static_cast(instance->getOrigin()); +} + +extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_Codec__1nGetEncodedImageFormat + (JNIEnv* env, jclass jclass, jlong ptr) { + SkCodec* instance = reinterpret_cast(static_cast(ptr)); + return static_cast(instance->getEncodedFormat()); +} + +extern "C" JNIEXPORT jint JNICALL Java_org_jetbrains_skija_Codec__1nReadPixels + (JNIEnv* env, jclass jclass, jlong ptr, jlong bitmapPtr, jint frame, jint priorFrame) { + SkCodec* instance = reinterpret_cast(static_cast(ptr)); + SkBitmap* bitmap = reinterpret_cast(static_cast(bitmapPtr)); + SkCodec::Options opts; + opts.fFrameIndex = frame; + opts.fPriorFrame = priorFrame; + SkCodec::Result result = instance->getPixels(bitmap->info(), bitmap->getPixels(), bitmap->rowBytes(), &opts); + return static_cast(result); +} + +extern "C" JNIEXPORT jint JNICALL Java_org_jetbrains_skija_Codec__1nGetFrameCount + (JNIEnv* env, jclass jclass, jlong ptr) { + SkCodec* instance = reinterpret_cast(static_cast(ptr)); + return instance->getFrameCount(); +} + +extern "C" JNIEXPORT jobject JNICALL Java_org_jetbrains_skija_Codec__1nGetFrameInfo + (JNIEnv* env, jclass jclass, jlong ptr, jint frame) { + SkCodec* instance = reinterpret_cast(static_cast(ptr)); + SkCodec::FrameInfo info; + instance->getFrameInfo(frame, &info); + return skija::AnimationFrameInfo::toJava(env, info); +} + +extern "C" JNIEXPORT jobject JNICALL Java_org_jetbrains_skija_Codec__1nGetFramesInfo + (JNIEnv* env, jclass jclass, jlong ptr, jint frame) { + SkCodec* instance = reinterpret_cast(static_cast(ptr)); + SkCodec::FrameInfo info; + std::vector frames = instance->getFrameInfo(); + jobjectArray res = env->NewObjectArray(frames.size(), skija::AnimationFrameInfo::cls, nullptr); + for (int i = 0; i < frames.size(); ++i) { + jobject infoObj = skija::AnimationFrameInfo::toJava(env, frames[i]); + env->SetObjectArrayElement(res, i, infoObj); + env->DeleteLocalRef(infoObj); + } + return res; +} + +extern "C" JNIEXPORT jint JNICALL Java_org_jetbrains_skija_Codec__1nGetRepetitionCount + (JNIEnv* env, jclass jclass, jlong ptr) { + SkCodec* instance = reinterpret_cast(static_cast(ptr)); + return instance->getRepetitionCount(); +} \ No newline at end of file diff --git a/native/src/interop.cc b/native/src/interop.cc index c52f726a..3fffdc08 100644 --- a/native/src/interop.cc +++ b/native/src/interop.cc @@ -126,6 +126,42 @@ namespace java { } namespace skija { + namespace AnimationFrameInfo { + jclass cls; + jmethodID ctor; + + void onLoad(JNIEnv* env) { + jclass local = env->FindClass("org/jetbrains/skija/AnimationFrameInfo"); + cls = static_cast(env->NewGlobalRef(local)); + ctor = env->GetMethodID(cls, "", "(IIZIZIILorg/jetbrains/skija/IRect;)V"); + } + + void onUnload(JNIEnv* env) { + env->DeleteGlobalRef(cls); + } + + jobject toJava(JNIEnv* env, const SkCodec::FrameInfo& i) { + SkBlendMode blend; + switch (i.fBlend) { + case SkCodecAnimation::Blend::kSrcOver: + blend = SkBlendMode::kSrcOver; + break; + case SkCodecAnimation::Blend::kSrc: + blend = SkBlendMode::kSrc; + break; + } + return env->NewObject(cls, ctor, + i.fRequiredFrame, + i.fDuration, + i.fFullyReceived, + static_cast(i.fAlphaType), + i.fHasAlphaWithinBounds, + static_cast(i.fDisposalMethod), + static_cast(blend), + IRect::fromSkIRect(env, i.fFrameRect)); + } + } + namespace Color4f { jclass cls; jmethodID ctor; @@ -684,6 +720,7 @@ namespace skija { } void onLoad(JNIEnv* env) { + AnimationFrameInfo::onLoad(env); Color4f::onLoad(env); Drawable::onLoad(env); FontFamilyName::onLoad(env); @@ -724,6 +761,7 @@ namespace skija { FontFamilyName::onUnload(env); Drawable::onUnload(env); Color4f::onUnload(env); + AnimationFrameInfo::onUnload(env); } } std::unique_ptr skMatrix(JNIEnv* env, jfloatArray matrixArray) { @@ -891,7 +929,7 @@ jlong packIPoint(SkIPoint p) { return packTwoInts(p.fX, p.fY); } -jlong packTwoInt32(SkISize p) { +jlong packISize(SkISize p) { return packTwoInts(p.fWidth, p.fHeight); } diff --git a/native/src/interop.hh b/native/src/interop.hh index e68801d6..25acc7f4 100644 --- a/native/src/interop.hh +++ b/native/src/interop.hh @@ -2,6 +2,7 @@ #include #include #include +#include "SkCodec.h" #include "SkFontMetrics.h" #include "SkFontStyle.h" #include "SkImageInfo.h" @@ -70,6 +71,14 @@ namespace java { } namespace skija { + namespace AnimationFrameInfo { + extern jclass cls; + extern jmethodID ctor; + void onLoad(JNIEnv* env); + void onUnload(JNIEnv* env); + jobject toJava(JNIEnv* env, const SkCodec::FrameInfo& i); + } + namespace Color4f { extern jclass cls; extern jmethodID ctor; diff --git a/shared/src/main/java/org/jetbrains/skija/AnimationDisposalMethod.java b/shared/src/main/java/org/jetbrains/skija/AnimationDisposalMethod.java new file mode 100644 index 00000000..d5c9e782 --- /dev/null +++ b/shared/src/main/java/org/jetbrains/skija/AnimationDisposalMethod.java @@ -0,0 +1,36 @@ +package org.jetbrains.skija; + +import lombok.Data; +import org.jetbrains.annotations.*; + +/** + *

This specifies how the next frame is based on this frame.

+ * + *

Names are based on the GIF 89a spec.

+ */ +public enum AnimationDisposalMethod { + @ApiStatus.Internal + _UNUSED, + + /** + *

The next frame should be drawn on top of this one.

+ * + *

In a GIF, a value of 0 (not specified) is also treated as KEEP.

+ */ + KEEP, + + /** + *

Similar to KEEP, except the area inside this frame's rectangle + * should be cleared to the BackGround color (transparent) before + * drawing the next frame.

+ */ + RESTORE_BG_COLOR, + + /** + *

The next frame should be drawn on top of the previous frame - i.e. + * disregarding this one.

+ * + *

In a GIF, a value of 4 is also treated as RestorePrevious.

+ */ + RESTORE_PREVIOUS; +} \ No newline at end of file diff --git a/shared/src/main/java/org/jetbrains/skija/AnimationFrameInfo.java b/shared/src/main/java/org/jetbrains/skija/AnimationFrameInfo.java new file mode 100644 index 00000000..02c19d8a --- /dev/null +++ b/shared/src/main/java/org/jetbrains/skija/AnimationFrameInfo.java @@ -0,0 +1,81 @@ +package org.jetbrains.skija; + +import lombok.*; +import org.jetbrains.annotations.*; + +/** + * Information about individual frames in a multi-framed image. + */ +@lombok.Data @AllArgsConstructor @With +public class AnimationFrameInfo { + @ApiStatus.Internal + public AnimationFrameInfo(int requiredFrame, int duration, boolean fullyReceived, int alphaTypeOrdinal, boolean hasAlphaWithinBounds, int disposalMethodOrdinal, int blendModeOrdinal, IRect frameRect) { + this(requiredFrame, duration, fullyReceived, ColorAlphaType.values()[alphaTypeOrdinal], hasAlphaWithinBounds, AnimationDisposalMethod.values()[disposalMethodOrdinal], BlendMode.values()[blendModeOrdinal], frameRect); + } + + /** + *

The frame that this frame needs to be blended with, or + * -1 if this frame is independent (so it can be + * drawn over an uninitialized buffer).

+ * + *

Note that this is the *earliest* frame that can be used + * for blending. Any frame from [_requiredFrame, i) can be + * used, unless its getDisposalMethod() is {@link AnimationDisposalMethod#RESTORE_PREVIOUS}.

+ */ + @ApiStatus.Internal + public int _requiredFrame; + + /** + * Number of milliseconds to show this frame. + */ + @ApiStatus.Internal + public int _duration; + + /** + *

Whether the end marker for this frame is contained in the stream.

+ * + *

Note: this does not guarantee that an attempt to decode will be complete. + * There could be an error in the stream.

+ */ + @ApiStatus.Internal + public boolean _fullyReceived; + + /** + *

This is conservative; it will still return non-opaque if e.g. a + * color index-based frame has a color with alpha but does not use it.

+ */ + @ApiStatus.Internal + public ColorAlphaType _alphaType; + + /** + *

Whether the updated rectangle contains alpha.

+ * + *

This is conservative; it will still be set to true if e.g. a color + * index-based frame has a color with alpha but does not use it. In + * addition, it may be set to true, even if the final frame, after + * blending, is opaque.

+ */ + @ApiStatus.Internal + public boolean _hasAlphaWithinBounds; + + /** + *

How this frame should be modified before decoding the next one.

+ */ + @ApiStatus.Internal + public AnimationDisposalMethod _disposalMethod; + + /** + *

How this frame should blend with the prior frame.

+ */ + @ApiStatus.Internal + public BlendMode _blendMode; + + /** + *

The rectangle updated by this frame.

+ * + *

It may be empty, if the frame does not change the image. It will + * always be contained by {@link Codec#getSize()}. + */ + @ApiStatus.Internal + public IRect _frameRect; +} \ No newline at end of file diff --git a/shared/src/main/java/org/jetbrains/skija/Codec.java b/shared/src/main/java/org/jetbrains/skija/Codec.java new file mode 100644 index 00000000..a40caa92 --- /dev/null +++ b/shared/src/main/java/org/jetbrains/skija/Codec.java @@ -0,0 +1,340 @@ +package org.jetbrains.skija; + +import java.lang.ref.*; +import lombok.*; +import org.jetbrains.annotations.*; +import org.jetbrains.skija.impl.*; + +public class Codec extends Managed implements IHasImageInfo { + static { Library.staticLoad(); } + + @ApiStatus.Internal + public ImageInfo _imageInfo = null; + + @ApiStatus.Internal + public Codec(long ptr) { + super(ptr, _FinalizerHolder.PTR); + } + + /** + * If this data represents an encoded image that we know how to decode, + * return an Codec that can decode it. Otherwise throws IllegalArgumentException. + */ + public static Codec makeFromData(Data data) { + try { + Stats.onNativeCall(); + long ptr = _nMakeFromData(Native.getPtr(data)); + if (ptr == 0) + throw new IllegalArgumentException("Unsupported format"); + return new Codec(ptr); + } finally { + Reference.reachabilityFence(data); + } + } + + @Override @NotNull + public ImageInfo getImageInfo() { + try { + if (_imageInfo == null) { + Stats.onNativeCall(); + _imageInfo = _nGetImageInfo(_ptr); + } + return _imageInfo; + } finally { + Reference.reachabilityFence(this); + } + } + + @NotNull @Contract("-> new") + public IPoint getSize() { + try { + Stats.onNativeCall(); + return IPoint._makeFromLong(_nGetSize(_ptr)); + } finally { + Reference.reachabilityFence(this); + } + } + + @NotNull + public EncodedOrigin getEncodedOrigin() { + try { + Stats.onNativeCall(); + return EncodedOrigin.values()[_nGetEncodedOrigin(_ptr)]; + } finally { + Reference.reachabilityFence(this); + } + } + + @NotNull + public EncodedImageFormat getEncodedImageFormat() { + try { + Stats.onNativeCall(); + return EncodedImageFormat.values()[_nGetEncodedImageFormat(_ptr)]; + } finally { + Reference.reachabilityFence(this); + } + } + + /** + *

Decodes an image into a bitmap.

+ * + * @return decoded bitmap + */ + @NotNull @Contract("_ -> new") + public Bitmap readPixels() { + var bitmap = new Bitmap(); + bitmap.allocPixels(getImageInfo()); + readPixels(bitmap); + return bitmap; + } + + /** + *

Decodes an image into a bitmap.

+ * + *

Repeated calls to this function should give the same results, + * allowing the PixelRef to be immutable.

+ * + *

Bitmap specifies the description of the format (config, size) + * expected by the caller. This can simply be identical + * to the info returned by getImageInfo().

+ * + *

This contract also allows the caller to specify + * different output-configs, which the implementation can + * decide to support or not.

+ * + *

A size that does not match getImageInfo() implies a request + * to scale. If the generator cannot perform this scale, + * it will throw an exception.

+ * + *

If the info contains a non-null ColorSpace, the codec + * will perform the appropriate color space transformation.

+ * + *

If the caller passes in the ColorSpace that maps to the + * ICC profile reported by getICCProfile(), the color space + * transformation is a no-op.

+ * + *

If the caller passes a null SkColorSpace, no color space + * transformation will be done.

+ * + * @param bitmap the description of the format (config, size) expected by the caller + * @return this + */ + @NotNull @Contract("_ -> this") + public Codec readPixels(Bitmap bitmap) { + try { + Stats.onNativeCall(); + _validateResult(_nReadPixels(_ptr, Native.getPtr(bitmap), 0, -1)); + return this; + } finally { + Reference.reachabilityFence(bitmap); + } + } + + /** + *

Decodes a frame in a multi-frame image into a bitmap.

+ * + *

Repeated calls to this function should give the same results, + * allowing the PixelRef to be immutable.

+ * + *

Bitmap specifies the description of the format (config, size) + * expected by the caller. This can simply be identical + * to the info returned by getImageInfo().

+ * + *

This contract also allows the caller to specify + * different output-configs, which the implementation can + * decide to support or not.

+ * + *

A size that does not match getImageInfo() implies a request + * to scale. If the generator cannot perform this scale, + * it will throw an exception.

+ * + *

If the info contains a non-null ColorSpace, the codec + * will perform the appropriate color space transformation.

+ * + *

If the caller passes in the ColorSpace that maps to the + * ICC profile reported by getICCProfile(), the color space + * transformation is a no-op.

+ * + *

If the caller passes a null SkColorSpace, no color space + * transformation will be done.

+ * + * @param bitmap the description of the format (config, size) expected by the caller + * @param frame index of the frame in multi-frame image to decode + * @return this + */ + @NotNull @Contract("_ -> this") + public Codec readPixels(Bitmap bitmap, int frame) { + try { + Stats.onNativeCall(); + _validateResult(_nReadPixels(_ptr, Native.getPtr(bitmap), frame, -1)); + return this; + } finally { + Reference.reachabilityFence(bitmap); + } + } + + /** + *

Decodes a frame in a multi-frame image into a bitmap.

+ * + *

Repeated calls to this function should give the same results, + * allowing the PixelRef to be immutable.

+ * + *

Bitmap specifies the description of the format (config, size) + * expected by the caller. This can simply be identical + * to the info returned by getImageInfo().

+ * + *

This contract also allows the caller to specify + * different output-configs, which the implementation can + * decide to support or not.

+ * + *

A size that does not match getImageInfo() implies a request + * to scale. If the generator cannot perform this scale, + * it will throw an exception.

+ * + *

If the info contains a non-null ColorSpace, the codec + * will perform the appropriate color space transformation.

+ * + *

If the caller passes in the ColorSpace that maps to the + * ICC profile reported by getICCProfile(), the color space + * transformation is a no-op.

+ * + *

If the caller passes a null SkColorSpace, no color space + * transformation will be done.

+ * + * @param bitmap the description of the format (config, size) expected by the caller + * @param frame index of the frame in multi-frame image to decode + * @param priorFrame index of the frame already in bitmap, might be used to optimize retrieving current frame + * @return this + */ + @NotNull @Contract("_ -> this") + public Codec readPixels(Bitmap bitmap, int frame, int priorFrame) { + try { + Stats.onNativeCall(); + _validateResult(_nReadPixels(_ptr, Native.getPtr(bitmap), frame, priorFrame)); + return this; + } finally { + Reference.reachabilityFence(bitmap); + } + } + + /** + *

Return the number of frames in the image.

+ * + *

May require reading through the stream.

+ */ + public int getFrameCount() { + try { + Stats.onNativeCall(); + return _nGetFrameCount(_ptr); + } finally { + Reference.reachabilityFence(this); + } + } + + /** + *

Return info about a single frame.

+ * + *

Only supported by multi-frame images. Does not read through the stream, + * so it should be called after getFrameCount() to parse any frames that + * have not already been parsed.

+ */ + public AnimationFrameInfo getFrameInfo(int frame) { + try { + Stats.onNativeCall(); + return _nGetFrameInfo(_ptr, frame); + } finally { + Reference.reachabilityFence(this); + } + } + + /** + *

Return info about all the frames in the image.

+ * + *

May require reading through the stream to determine info about the + * frames (including the count).

+ * + *

As such, future decoding calls may require a rewind.

+ * + *

For still (non-animated) image codecs, this will return an empty array.

+ */ + public AnimationFrameInfo[] getFramesInfo() { + try { + Stats.onNativeCall(); + return _nGetFramesInfo(_ptr); + } finally { + Reference.reachabilityFence(this); + } + } + + /** + *

Return the number of times to repeat, if this image is animated. This number does not + * include the first play through of each frame. For example, a repetition count of 4 means + * that each frame is played 5 times and then the animation stops.

+ * + *

It can return -1, a negative number, meaning that the animation + * should loop forever.

+ * + *

May require reading the stream to find the repetition count.

+ * + *

As such, future decoding calls may require a rewind.

+ * + *

For still (non-animated) image codecs, this will return 0.

+ */ + public int getRepetitionCount() { + try { + Stats.onNativeCall(); + return _nGetRepetitionCount(_ptr); + } finally { + Reference.reachabilityFence(this); + } + } + + @ApiStatus.Internal + public static void _validateResult(int result) { + switch (result) { + case 1: // kIncompleteInput + throw new IllegalArgumentException("Incomplete input: A partial image was generated."); + + case 2: // kErrorInInput + throw new IllegalArgumentException("Error in input"); + + case 3: // kInvalidConversion + throw new IllegalArgumentException("Invalid conversion: The generator cannot convert to match the request, ignoring dimensions"); + + case 4: // kInvalidScale + throw new IllegalArgumentException("Invalid scale: The generator cannot scale to requested size"); + + case 5: // kInvalidParameters + throw new IllegalArgumentException("Invalid parameter: Parameters (besides info) are invalid. e.g. NULL pixels, rowBytes too small, etc"); + + case 6: // kInvalidInput + throw new IllegalArgumentException("Invalid input: The input did not contain a valid image"); + + case 7: // kCouldNotRewind + throw new UnsupportedOperationException("Could not rewind: Fulfilling this request requires rewinding the input, which is not supported for this input"); + + case 8: // kInternalError + throw new RuntimeException("Internal error"); + + case 9: // kUnimplemented + throw new UnsupportedOperationException("Unimplemented: This method is not implemented by this codec"); + } + } + + @ApiStatus.Internal + public static class _FinalizerHolder { + public static final long PTR = _nGetFinalizer(); + } + + @ApiStatus.Internal public static native long _nGetFinalizer(); + @ApiStatus.Internal public static native long _nMakeFromData(long dataPtr); + @ApiStatus.Internal public static native ImageInfo _nGetImageInfo(long ptr); + @ApiStatus.Internal public static native long _nGetSize(long ptr); + @ApiStatus.Internal public static native int _nGetEncodedOrigin(long ptr); + @ApiStatus.Internal public static native int _nGetEncodedImageFormat(long ptr); + @ApiStatus.Internal public static native int _nReadPixels(long ptr, long bitmapPtr, int frame, int priorFrame); + @ApiStatus.Internal public static native int _nGetFrameCount(long ptr); + @ApiStatus.Internal public static native AnimationFrameInfo _nGetFrameInfo(long ptr, int frame); + @ApiStatus.Internal public static native AnimationFrameInfo[] _nGetFramesInfo(long ptr); + @ApiStatus.Internal public static native int _nGetRepetitionCount(long ptr); +} diff --git a/shared/src/main/java/org/jetbrains/skija/EncodedOrigin.java b/shared/src/main/java/org/jetbrains/skija/EncodedOrigin.java new file mode 100644 index 00000000..22d593e1 --- /dev/null +++ b/shared/src/main/java/org/jetbrains/skija/EncodedOrigin.java @@ -0,0 +1,100 @@ +package org.jetbrains.skija; + +import org.jetbrains.annotations.*; + +public enum EncodedOrigin { + @ApiStatus.Internal + _UNUSED, + + /** + * Default + */ + TOP_LEFT, + + /** + * Reflected across y-axis + */ + TOP_RIGHT, + + /** + * Rotated 180 + */ + BOTTOM_RIGHT, + + /** + * Reflected across x-axis + */ + BOTTOM_LEFT, + + /** + * Reflected across x-axis, Rotated 90 CCW + */ + LEFT_TOP, + + /** + * Rotated 90 CW + */ + RIGHT_TOP, + + /** + * Reflected across x-axis, Rotated 90 CW + */ + RIGHT_BOTTOM, + + /** + * Rotated 90 CCW + */ + LEFT_BOTTOM; + + /** + * Given an encoded origin and the width and height of the source data, returns a matrix + * that transforms the source rectangle with upper left corner at [0, 0] and origin to a correctly + * oriented destination rectangle of [0, 0, w, h]. + */ + public Matrix33 toMatrix(int w, int h) { + switch (this) { + case TOP_LEFT: + return Matrix33.IDENTITY; + + case TOP_RIGHT: + return new Matrix33(-1, 0, w, 0, 1, 0, 0, 0, 1); + + case BOTTOM_RIGHT: + return new Matrix33(-1, 0, w, 0, -1, h, 0, 0, 1); + + case BOTTOM_LEFT: + return new Matrix33( 1, 0, 0, 0, -1, h, 0, 0, 1); + + case LEFT_TOP: + return new Matrix33( 0, 1, 0, 1, 0, 0, 0, 0, 1); + + case RIGHT_TOP: + return new Matrix33( 0, -1, w, 1, 0, 0, 0, 0, 1); + + case RIGHT_BOTTOM: + return new Matrix33( 0, -1, w, -1, 0, h, 0, 0, 1); + + case LEFT_BOTTOM: + return new Matrix33( 0, 1, 0, -1, 0, h, 0, 0, 1); + + default: + throw new IllegalArgumentException("Unsupported origin " + this); + } + } + + /** + * Return true if the encoded origin includes a 90 degree rotation, in which case the width + * and height of the source data are swapped relative to a correctly oriented destination. + */ + public boolean swapsWidthHeight() { + switch (this) { + case LEFT_TOP: + case RIGHT_TOP: + case RIGHT_BOTTOM: + case LEFT_BOTTOM: + return true; + default: + return false; + } + } +} diff --git a/shared/src/main/java/org/jetbrains/skija/IPoint.java b/shared/src/main/java/org/jetbrains/skija/IPoint.java index 7764f9e1..c14ce733 100644 --- a/shared/src/main/java/org/jetbrains/skija/IPoint.java +++ b/shared/src/main/java/org/jetbrains/skija/IPoint.java @@ -27,4 +27,9 @@ public IPoint offset(@NotNull IPoint vec) { public boolean isEmpty() { return _x <= 0 || _y <= 0; } + + @ApiStatus.Internal + public static IPoint _makeFromLong(long l) { + return new IPoint((int) (l >>> 32), (int) (l & 0xFFFFFFFF)); + } } \ No newline at end of file diff --git a/shared/src/main/java/org/jetbrains/skija/IRange.java b/shared/src/main/java/org/jetbrains/skija/IRange.java index f82b08fc..e897b7e9 100644 --- a/shared/src/main/java/org/jetbrains/skija/IRange.java +++ b/shared/src/main/java/org/jetbrains/skija/IRange.java @@ -1,9 +1,18 @@ package org.jetbrains.skija; import lombok.Data; +import org.jetbrains.annotations.*; @Data public class IRange { + @ApiStatus.Internal public final int _start; + + @ApiStatus.Internal public final int _end; + + @ApiStatus.Internal + public static IRange _makeFromLong(long l) { + return new IRange((int) (l >>> 32), (int) (l & 0xFFFFFFFF)); + } } \ No newline at end of file diff --git a/shared/src/main/java/org/jetbrains/skija/paragraph/Paragraph.java b/shared/src/main/java/org/jetbrains/skija/paragraph/Paragraph.java index b9858e7a..8091fc5b 100644 --- a/shared/src/main/java/org/jetbrains/skija/paragraph/Paragraph.java +++ b/shared/src/main/java/org/jetbrains/skija/paragraph/Paragraph.java @@ -146,8 +146,7 @@ public PositionWithAffinity getGlyphPositionAtCoordinate(float dx, float dy) { public IRange getWordBoundary(int offset) { try { Stats.onNativeCall(); - long l = _nGetWordBoundary(_ptr, offset); - return new IRange((int) (l >>> 32), (int) (l & 0xFFFFFFFF)); + return IRange._makeFromLong(_nGetWordBoundary(_ptr, offset)); } finally { Reference.reachabilityFence(this); }