iOS: implement Image Pixel

This commit is contained in:
pengfei.zhou 2021-11-22 11:54:47 +08:00 committed by osborn
parent 190eb4afd7
commit b29f2d6a4e
20 changed files with 365 additions and 9 deletions

View File

@ -606,4 +606,15 @@ public class ImageNode extends ViewNode<ImageView> {
return new JavaValue(); return new JavaValue();
} }
} }
@DoricMethod
public void setImagePixels(JSObject prop) {
int width = prop.asObject().getProperty("width").asNumber().toInt();
int height = prop.asObject().getProperty("height").asNumber().toInt();
byte[] pixels = prop.asObject().getProperty("pixels").asArrayBuffer().value();
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
ByteBuffer byteBuffer = ByteBuffer.wrap(pixels);
bitmap.copyPixelsFromBuffer(byteBuffer);
mView.setImageBitmap(bitmap);
}
} }

View File

@ -0,0 +1,65 @@
import {
Base64Resource,
DrawableResource,
Group,
Panel,
jsx,
Color,
layoutConfig,
Image,
RemoteResource,
MainBundleResource,
Scroller,
VLayout,
Text,
Gravity,
createRef,
loge,
} from "doric";
import { colors, label } from "./utils";
import { img_base64 } from "./image_base64";
@Entry
export class ImageProcessorDemo extends Panel {
build(root: Group): void {
const iv = createRef<Image>();
<Scroller parent={root} layoutConfig={layoutConfig().most()}>
<VLayout
layoutConfig={layoutConfig().mostWidth().fitHeight()}
space={10}
gravity={Gravity.Center}
>
<Text
layoutConfig={layoutConfig().mostWidth().justHeight()}
textSize={30}
textColor={Color.WHITE}
backgroundColor={colors[5]}
textAlignment={Gravity.Center}
height={50}
>
Image Processor
</Text>
<Image ref={iv} imageUrl="https://doric.pub/logo.png" />
<Text
onClick={async () => {
const imageInfo = await iv.current.getImageInfo(context);
loge(imageInfo);
const pixels = await iv.current.getImagePixels(context);
loge(pixels.byteLength);
const data = new Uint8Array(pixels);
for (let i = 0; i < data.length - 4; i += 4) {
data[i + 3] = 12;
}
iv.current.imagePixels = {
width: imageInfo.width,
height: imageInfo.height,
pixels: pixels,
};
}}
>
Transparent
</Text>
</VLayout>
</Scroller>;
}
}

View File

@ -30,7 +30,7 @@ export class ResourceDemo extends Panel {
loge(pixels.byteLength); loge(pixels.byteLength);
const data = new Uint8Array(pixels); const data = new Uint8Array(pixels);
for (let i = 0; i < data.length - 4; i += 4) { for (let i = 0; i < data.length - 4; i += 4) {
data[i + 3] = 12; data[i + 3] = 0xff / 3;
} }
iv.current.imagePixels = { iv.current.imagePixels = {
width: imageInfo.width, width: imageInfo.width,
@ -63,12 +63,12 @@ export class ResourceDemo extends Panel {
} }
/> />
<Image <Image
ref={iv}
image={ image={
new RemoteResource("https://p.upyun.com/demo/webp/webp/jpg-0.webp") new RemoteResource("https://p.upyun.com/demo/webp/webp/jpg-0.webp")
} }
/> />
<Image <Image
ref={iv}
image={new Base64Resource(img_base64[0])} image={new Base64Resource(img_base64[0])}
onClick={async () => { onClick={async () => {
await click(); await click();

View File

@ -79,6 +79,18 @@
ReferencedContainer = "container:Example.xcodeproj"> ReferencedContainer = "container:Example.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "CGBITMAP_CONTEXT_LOG_ERRORS"
value = "TRUE"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "CG_CONTEXT_SHOW_BACKTRACE"
value = "TRUE"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -20,8 +20,14 @@
// Created by pengfei.zhou on 2019/7/25. // Created by pengfei.zhou on 2019/7/25.
// //
#import <DoricCore/DoricExtensions.h>
#import "DoricJSCoreExecutor.h" #import "DoricJSCoreExecutor.h"
void ReleaseArrayBufferData(void *bytes, void *deallocatorContext) {
id data = (__bridge_transfer id) deallocatorContext;
data = nil;
}
@interface DoricJSCoreExecutor () @interface DoricJSCoreExecutor ()
@property(nonatomic, strong) JSContext *jsContext; @property(nonatomic, strong) JSContext *jsContext;
@ -57,7 +63,19 @@ - (void)injectGlobalJSObject:(NSString *)name obj:(id)obj {
- (JSValue *)invokeObject:(NSString *)objName method:(NSString *)funcName args:(NSArray *)args { - (JSValue *)invokeObject:(NSString *)objName method:(NSString *)funcName args:(NSArray *)args {
JSValue *obj = [self.jsContext objectForKeyedSubscript:objName]; JSValue *obj = [self.jsContext objectForKeyedSubscript:objName];
JSValue *ret = [obj invokeMethod:funcName withArguments:args]; JSValue *ret = [obj invokeMethod:funcName withArguments:[args map:^id(id obj) {
if ([obj isKindOfClass:NSData.class]) {
NSData *data = (NSData *) obj;
JSObjectRef jsObject = JSObjectMakeArrayBufferWithBytesNoCopy(self.jsContext.JSGlobalContextRef,
(void *) data.bytes,
data.length,
ReleaseArrayBufferData,
(__bridge_retained void *) data,
NULL);
return [JSValue valueWithJSValueRef:jsObject inContext:self.jsContext];
}
return obj;
}]];
[self checkJSException]; [self checkJSException];
return ret; return ret;
} }

View File

@ -0,0 +1,12 @@
//
// Created by pengfei.zhou on 2021/11/19.
//
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@interface JSValue (Doric)
- (BOOL)isArrayBuffer;
- (NSData *)toArrayBuffer;
@end

View File

@ -0,0 +1,31 @@
//
// Created by pengfei.zhou on 2021/11/19.
//
#import "JSValue+Doric.h"
@implementation JSValue (Doric)
- (BOOL)isArrayBuffer {
JSContextRef ctx = self.context.JSGlobalContextRef;
JSValueRef jsValueRef = self.JSValueRef;
if (self.isObject) {
JSTypedArrayType type = JSValueGetTypedArrayType(ctx, jsValueRef, NULL);
return type == kJSTypedArrayTypeArrayBuffer;
}
return NO;
}
- (NSData *)toArrayBuffer {
if (!self.isArrayBuffer) {
return nil;
}
JSContextRef ctx = self.context.JSGlobalContextRef;
JSValueRef jsValueRef = self.JSValueRef;
JSObjectRef ref = JSValueToObject(ctx, jsValueRef, NULL);
size_t size = JSObjectGetArrayBufferByteLength(ctx, ref, NULL);
void *ptr = JSObjectGetArrayBufferBytesPtr(ctx, ref, NULL);
return [[NSData alloc] initWithBytesNoCopy:ptr length:size freeWhenDone:NO];
}
@end

View File

@ -25,6 +25,8 @@
#import "DoricUtil.h" #import "DoricUtil.h"
#import "DoricSuperNode.h" #import "DoricSuperNode.h"
#import "DoricThirdParty.h" #import "DoricThirdParty.h"
#import <JavaScriptCore/JavaScriptCore.h>
#import <JSValue+Doric.h>
#if DORIC_USE_YYWEBIMAGE #if DORIC_USE_YYWEBIMAGE
@ -576,6 +578,33 @@ - (void)blendView:(UIImageView *)view forPropName:(NSString *)name propValue:(id
options:NSKeyValueObservingOptionNew options:NSKeyValueObservingOptionNew
context:nil]; context:nil];
#endif #endif
} else if ([@"imagePixels" isEqualToString:name]) {
NSDictionary *imagePixels = prop;
NSUInteger width = [imagePixels[@"width"] unsignedIntValue];
NSUInteger height = [imagePixels[@"height"] unsignedIntValue];
NSString *pixelsCallbackId = imagePixels[@"pixels"];
[[self callJSResponse:pixelsCallbackId, nil] setResultCallback:^(JSValue *pixelsValue) {
if (![pixelsValue isArrayBuffer]) {
return;
}
NSData *data = [pixelsValue toArrayBuffer];
[self.doricContext.driver ensureSyncInMainQueue:^{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate((void *) data.bytes,
width,
height,
8,
width * 4,
colorSpace,
kCGImageAlphaPremultipliedLast);
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:UIScreen.mainScreen.scale orientation:UIImageOrientationUp];
CGImageRelease(imageRef);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
self.view.image = image;
}];
}];
} else { } else {
[super blendView:view forPropName:name propValue:prop]; [super blendView:view forPropName:name propValue:prop];
} }
@ -711,5 +740,35 @@ - (void)dealloc {
[(DoricImageView *) self.view removeObserver:self forKeyPath:@"currentFrameIndex" context:nil]; [(DoricImageView *) self.view removeObserver:self forKeyPath:@"currentFrameIndex" context:nil];
#endif #endif
} }
- (NSDictionary *)getImageInfo {
CGImageRef imageRef = [self.view.image CGImage];
return @{
@"width": @(CGImageGetWidth(imageRef)),
@"height": @(CGImageGetHeight(imageRef)),
};
}
- (NSData *)getImagePixels {
CGImageRef imageRef = [self.view.image CGImage];
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
size_t bytesPerPixel = 4;
size_t bytesPerRow = bytesPerPixel * width;
unsigned char *imageData = malloc(width * height * bytesPerPixel);
CGContextRef contextRef = CGBitmapContextCreate(
imageData,
width,
height,
8,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast);
CGContextDrawImage(contextRef, CGRectMake(0, 0, width, height), imageRef);
CGColorSpaceRelease(colorSpace);
CGContextRelease(contextRef);
return [[NSData alloc] initWithBytesNoCopy:imageData length:width * height * bytesPerPixel];
} }
@end @end

View File

@ -2316,7 +2316,30 @@ var Image = /** @class */ (function (_super) {
Image.prototype.getImagePixels = function (context) { Image.prototype.getImagePixels = function (context) {
return this.nativeChannel(context, "getImagePixels")(); return this.nativeChannel(context, "getImagePixels")();
}; };
<<<<<<< HEAD
__decorate$b([ __decorate$b([
=======
Image.prototype.setImagePixels = function (context, imagePixels) {
if (Environment.platform === 'iOS') {
imagePixels.pixels = context.function2Id(function () {
return imagePixels.pixels;
});
}
return this.nativeChannel(context, "setImagePixels")(imagePixels);
};
Image.prototype.toModel = function () {
var ret = _super.prototype.toModel.call(this);
if (Environment.platform === 'iOS') {
if (Reflect.has(ret.props, "imagePixels")) {
var imagePixels = Reflect.get(ret.props, "imagePixels");
var pixels_1 = imagePixels.pixels;
imagePixels.pixels = this.callback2Id(function () { return pixels_1; });
}
}
return ret;
};
__decorate$a([
>>>>>>> 60c3fff0... iOS: implement Image Pixel
Property, Property,
__metadata$b("design:type", Object) __metadata$b("design:type", Object)
], Image.prototype, "imagePixels", void 0); ], Image.prototype, "imagePixels", void 0);

View File

@ -1726,6 +1726,25 @@ class Image extends View {
getImagePixels(context) { getImagePixels(context) {
return this.nativeChannel(context, "getImagePixels")(); return this.nativeChannel(context, "getImagePixels")();
} }
setImagePixels(context, imagePixels) {
if (Environment.platform === 'iOS') {
imagePixels.pixels = context.function2Id(() => {
return imagePixels.pixels;
});
}
return this.nativeChannel(context, "setImagePixels")(imagePixels);
}
toModel() {
const ret = super.toModel();
if (Environment.platform === 'iOS') {
if (Reflect.has(ret.props, "imagePixels")) {
const imagePixels = Reflect.get(ret.props, "imagePixels");
const pixels = imagePixels.pixels;
imagePixels.pixels = this.callback2Id(() => pixels);
}
}
return ret;
}
} }
__decorate$b([ __decorate$b([
Property, Property,

View File

@ -3254,6 +3254,25 @@ class Image extends View {
getImagePixels(context) { getImagePixels(context) {
return this.nativeChannel(context, "getImagePixels")(); return this.nativeChannel(context, "getImagePixels")();
} }
setImagePixels(context, imagePixels) {
if (Environment.platform === 'iOS') {
imagePixels.pixels = context.function2Id(() => {
return imagePixels.pixels;
});
}
return this.nativeChannel(context, "setImagePixels")(imagePixels);
}
toModel() {
const ret = super.toModel();
if (Environment.platform === 'iOS') {
if (Reflect.has(ret.props, "imagePixels")) {
const imagePixels = Reflect.get(ret.props, "imagePixels");
const pixels = imagePixels.pixels;
imagePixels.pixels = this.callback2Id(() => pixels);
}
}
return ret;
}
} }
__decorate$b([ __decorate$b([
Property, Property,

9
doric-js/index.d.ts vendored
View File

@ -248,6 +248,7 @@ declare module 'doric/lib/src/ui/view' {
onClick?: Function; onClick?: Function;
superview?: Superview; superview?: Superview;
callbacks: Map<String, Function>; callbacks: Map<String, Function>;
callback2Id(f: Function): string;
findViewByTag(tag: string): View | undefined; findViewByTag(tag: string): View | undefined;
constructor(); constructor();
/** Anchor start*/ /** Anchor start*/
@ -616,7 +617,7 @@ declare module 'doric/lib/src/widget/text' {
} }
declare module 'doric/lib/src/widget/image' { declare module 'doric/lib/src/widget/image' {
import { View } from "doric/lib/src/ui/view"; import { View, NativeViewModel } from "doric/lib/src/ui/view";
import { Color } from "doric/lib/src/util/color"; import { Color } from "doric/lib/src/util/color";
import { BridgeContext } from "doric/lib/src/runtime/global"; import { BridgeContext } from "doric/lib/src/runtime/global";
import { Resource } from "doric/lib/src/util/resource"; import { Resource } from "doric/lib/src/util/resource";
@ -713,6 +714,12 @@ declare module 'doric/lib/src/widget/image' {
mimeType: string; mimeType: string;
}>; }>;
getImagePixels(context: BridgeContext): Promise<ArrayBuffer>; getImagePixels(context: BridgeContext): Promise<ArrayBuffer>;
setImagePixels(context: BridgeContext, imagePixels: {
width: number;
height: number;
pixels: ArrayBuffer;
}): Promise<void>;
toModel(): NativeViewModel;
} }
export function image(config: Partial<Image>): Image; export function image(config: Partial<Image>): Image;
} }

View File

@ -62,7 +62,7 @@ export declare abstract class View implements Modeling {
onClick?: Function; onClick?: Function;
superview?: Superview; superview?: Superview;
callbacks: Map<String, Function>; callbacks: Map<String, Function>;
private callback2Id; callback2Id(f: Function): string;
private id2Callback; private id2Callback;
findViewByTag(tag: string): View | undefined; findViewByTag(tag: string): View | undefined;
constructor(); constructor();

View File

@ -1,4 +1,4 @@
import { View } from "../ui/view"; import { View, NativeViewModel } from "../ui/view";
import { Color } from "../util/color"; import { Color } from "../util/color";
import { BridgeContext } from "../runtime/global"; import { BridgeContext } from "../runtime/global";
import { Resource } from "../util/resource"; import { Resource } from "../util/resource";
@ -95,5 +95,11 @@ export declare class Image extends View {
mimeType: string; mimeType: string;
}>; }>;
getImagePixels(context: BridgeContext): Promise<ArrayBuffer>; getImagePixels(context: BridgeContext): Promise<ArrayBuffer>;
setImagePixels(context: BridgeContext, imagePixels: {
width: number;
height: number;
pixels: ArrayBuffer;
}): Promise<void>;
toModel(): NativeViewModel;
} }
export declare function image(config: Partial<Image>): Image; export declare function image(config: Partial<Image>): Image;

View File

@ -48,6 +48,25 @@ export class Image extends View {
getImagePixels(context) { getImagePixels(context) {
return this.nativeChannel(context, "getImagePixels")(); return this.nativeChannel(context, "getImagePixels")();
} }
setImagePixels(context, imagePixels) {
if (Environment.platform === 'iOS') {
imagePixels.pixels = context.function2Id(() => {
return imagePixels.pixels;
});
}
return this.nativeChannel(context, "setImagePixels")(imagePixels);
}
toModel() {
const ret = super.toModel();
if (Environment.platform === 'iOS') {
if (Reflect.has(ret.props, "imagePixels")) {
const imagePixels = Reflect.get(ret.props, "imagePixels");
const pixels = imagePixels.pixels;
imagePixels.pixels = this.callback2Id(() => pixels);
}
}
return ret;
}
} }
__decorate([ __decorate([
Property, Property,

View File

@ -144,7 +144,8 @@ export abstract class View implements Modeling {
superview?: Superview superview?: Superview
callbacks!: Map<String, Function> callbacks!: Map<String, Function>
private callback2Id(f: Function) {
callback2Id(f: Function) {
if (this.callbacks === undefined) { if (this.callbacks === undefined) {
this.callbacks = new Map this.callbacks = new Map
} }

View File

@ -129,7 +129,7 @@ export abstract class View implements Modeling {
callbacks: Map<String, Function> = new Map callbacks: Map<String, Function> = new Map
private callback2Id(f: Function) { callback2Id(f: Function) {
const id = uniqueId('Function') const id = uniqueId('Function')
this.callbacks.set(id, f) this.callbacks.set(id, f)
return id return id

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { View, Property } from "../ui/view" import { View, Property, NativeViewModel } from "../ui/view"
import { layoutConfig } from "../util/layoutconfig" import { layoutConfig } from "../util/layoutconfig"
import { Color } from "../util/color" import { Color } from "../util/color"
import { BridgeContext } from "../runtime/global" import { BridgeContext } from "../runtime/global"
@ -149,6 +149,34 @@ export class Image extends View {
getImagePixels(context: BridgeContext): Promise<ArrayBuffer> { getImagePixels(context: BridgeContext): Promise<ArrayBuffer> {
return this.nativeChannel(context, "getImagePixels")() return this.nativeChannel(context, "getImagePixels")()
} }
setImagePixels(
context: BridgeContext,
imagePixels: {
width: number,
height: number,
pixels: ArrayBuffer
}): Promise<void> {
if (Environment.platform === 'iOS') {
(imagePixels.pixels as unknown as any) = context.function2Id(() => {
return imagePixels.pixels
})
}
return this.nativeChannel(context, "setImagePixels")(imagePixels)
}
toModel() {
const ret = super.toModel()
if (Environment.platform === 'iOS') {
if (Reflect.has(ret.props, "imagePixels")) {
const imagePixels = Reflect.get(ret.props, "imagePixels")
const pixels = imagePixels.pixels
imagePixels.pixels = this.callback2Id(() => pixels)
}
}
return ret
}
} }
export function image(config: Partial<Image>) { export function image(config: Partial<Image>) {

View File

@ -3328,6 +3328,25 @@ class Image extends View {
getImagePixels(context) { getImagePixels(context) {
return this.nativeChannel(context, "getImagePixels")(); return this.nativeChannel(context, "getImagePixels")();
} }
setImagePixels(context, imagePixels) {
if (Environment.platform === 'iOS') {
imagePixels.pixels = context.function2Id(() => {
return imagePixels.pixels;
});
}
return this.nativeChannel(context, "setImagePixels")(imagePixels);
}
toModel() {
const ret = super.toModel();
if (Environment.platform === 'iOS') {
if (Reflect.has(ret.props, "imagePixels")) {
const imagePixels = Reflect.get(ret.props, "imagePixels");
const pixels = imagePixels.pixels;
imagePixels.pixels = this.callback2Id(() => pixels);
}
}
return ret;
}
} }
__decorate$b([ __decorate$b([
Property, Property,

File diff suppressed because one or more lines are too long