Full Code of cyrildiagne/ar-cutpaste for AI

master 155fddc9b16d cached
19 files
26.8 KB
7.8k tokens
15 symbols
1 requests
Download .txt
Repository: cyrildiagne/ar-cutpaste
Branch: master
Commit: 155fddc9b16d
Files: 19
Total size: 26.8 KB

Directory structure:
gitextract_7t568dfp/

├── .gitignore
├── LICENSE
├── README.md
├── app/
│   ├── .expo-shared/
│   │   └── assets.json
│   ├── .gitignore
│   ├── App.tsx
│   ├── README.md
│   ├── app.json
│   ├── babel.config.js
│   ├── components/
│   │   ├── Base64.tsx
│   │   ├── ProgressIndicator.tsx
│   │   └── Server.tsx
│   ├── package.json
│   └── tsconfig.json
└── server/
    ├── README.md
    ├── requirements.txt
    └── src/
        ├── main.py
        ├── ps.py
        └── script.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
venv
.vscode
.DS_Store
.pyc
node_modules
__pycache__
server/*.png
server/*.jpg
*.psd

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Cyril Diagne

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# AR Cut & Paste

An AR+ML prototype that allows cutting elements from your surroundings and pasting them in an image editing software.

Although only Photoshop is being handled currently, it may handle different outputs in the future.

Demo & more infos: [Thread](https://twitter.com/cyrildiagne/status/1256916982764646402)

⚠️ This is a research prototype and not a consumer / photoshop user tool.

**Update 2020.05.11:** If you're looking for an easy to use app based on this research, head over to https://clipdrop.co

## Modules

This prototype runs as 3 independent modules:

- **The mobile app**

  - Check out the [/app](/app) folder for instructions on how to deploy the app to your mobile.

- **The local server**

  - The interface between the mobile app and Photoshop.
  - It finds the position pointed on screen by the camera using [screenpoint](https://github.com/cyrildiagne/screenpoint)
  - Check out the [/server](/server) folder for instructions on configuring the local server

- **The object detection / background removal service**

  - For now, the salience detection and background removal are delegated to an external service
  - It would be a lot simpler to use something like [DeepLap](https://github.com/shaqian/tflite-react-native) directly within the mobile app. But that hasn't been implemented in this repo yet.

## Usage

### 1 - Configure Photoshop

- Go to "Preferences > Plug-ins", enable "Remote Connection" and set a friendly password that you'll need later.
- Make sure that your PS document settings match those in ```server/src/ps.py```, otherwise only an empty layer will be pasted.
- Also make sure that your document has some sort of background. If the background is just blank, SIFT will probably not have enough feature to do a correct match.

<!--
### 2) Setup the local server

```bash
virtualenv venv
source venv/bin/activate
pip install -r requirements.txt
``` -->

### 2 - Setup the external salience object detection service

#### Option 1: Set up your own model service (requires a CUDA GPU)

- As mentioned above, for the time being, you must deploy the
BASNet model (Qin & al, CVPR 2019) as an external HTTP service using this [BASNet-HTTP wrapper](https://github.com/cyrildiagne/basnet-http) (requires a CUDA GPU)

- You will need the deployed service URL to configure the local server

- Make sure to configure a different port if you're running BASNet on the same computer as the local service

#### Option 2: Use a community provided endpoint

A public endpoint has been provided by members of the community. This is useful if you don't have your own CUDA GPU or do not want to go through the process of running the servce on your own.

Use this endpoint by launching the local server with `--basnet_service_ip http://u2net-predictor.tenant-compass.global.coreweave.com`

### 3 - Configure and run the local server

- Follow the instructions in [/server](/server) to setup & run the local server.

### 4 - Configure and run the mobile app

- Follow the instructions in [/app](/app) to setup & deploy the mobile app.

## Thanks and Acknowledgements

- [BASNet code](https://github.com/NathanUA/BASNet) for '[*BASNet: Boundary-Aware Salient Object Detection*](http://openaccess.thecvf.com/content_CVPR_2019/html/Qin_BASNet_Boundary-Aware_Salient_Object_Detection_CVPR_2019_paper.html) [code](https://github.com/NathanUA/BASNet)', [Xuebin Qin](https://webdocs.cs.ualberta.ca/~xuebin/), [Zichen Zhang](https://webdocs.cs.ualberta.ca/~zichen2/), [Chenyang Huang](https://chenyangh.com/), [Chao Gao](https://cgao3.github.io/), [Masood Dehghan](https://sites.google.com/view/masoodd) and [Martin Jagersand](https://webdocs.cs.ualberta.ca/~jag/)
- RunwayML for the [Photoshop paste code](https://github.com/runwayml/RunwayML-for-Photoshop/blob/master/host/index.jsx)
- [CoreWeave](https://www.coreweave.com) for hosting the public U^2Net model endpoint on Tesla V100s


================================================
FILE: app/.expo-shared/assets.json
================================================
{
  "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
  "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}


================================================
FILE: app/.gitignore
================================================
node_modules/**/*
.expo/*
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
web-report/

# macOS
.DS_Store


================================================
FILE: app/App.tsx
================================================
import React, { useState, useEffect } from "react";
import {
  Text,
  View,
  Image,
  TouchableWithoutFeedback,
  StyleSheet,
} from "react-native";
import * as ImageManipulator from "expo-image-manipulator";
import { Camera } from "expo-camera";

import ProgressIndicator from "./components/ProgressIndicator";
import server from "./components/Server";

const styles = StyleSheet.create({
  resultImgView: {
    position: "absolute",
    zIndex: 200,
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
  },
  resultImg: {
    position: "absolute",
    zIndex: 300,
    top: "25%",
    left: 0,
    width: "100%",
    height: "50%",
  },
});

interface State {
  hasPermission: boolean;
  type: any;
  camera: any;
  currImgSrc: string | null;
}

export default function App() {
  const [state, setState] = useState({
    hasPermission: false,
    type: Camera.Constants.Type.back,
    camera: null,
    currImgSrc: "",
  } as State);

  const [pressed, setPressed] = useState(false);
  const [pasting, setPasting] = useState(false);

  let camera: any = null;

  useEffect(() => {
    (async () => {
      // Ping the server on startup.
      server.ping();
      // Request permission.
      const { status } = await Camera.requestPermissionsAsync();
      const hasPermission = status === "granted" ? true : false;
      setState({ ...state, hasPermission });
    })();
  }, []);

  async function cut(): Promise<string> {
    const start = Date.now();
    console.log("");
    console.log("Cut");

    console.log(camera.pictureSize);
    // const ratios = await camera.getSupportedRatiosAsync()
    // console.log(ratios)
    // const sizes = await camera.getAvailablePictureSizeAsync("2:1")
    // console.log(sizes)

    console.log("> taking image...");
    const opts = { skipProcessing: true, exif: false, quality: 0 };
    // const opts = {};
    let photo = await camera.takePictureAsync(opts);

    console.log("> resizing...");
    const { uri } = await ImageManipulator.manipulateAsync(
      photo.uri,
      [
        { resize: { width: 256, height: 512 } },
        { crop: { originX: 0, originY: 128, width: 256, height: 256 } },
        // { resize: { width: 256, height: 457 } },
        // { crop: { originX: 0, originY: 99, width: 256, height: 256 } },
        // { resize: { width: 256, height: 341 } },
        // { crop: { originX: 0, originY: 42, width: 256, height: 256 } },
      ]
      // { compress: 0, format: ImageManipulator.SaveFormat.JPEG, base64: false }
    );

    console.log("> sending to /cut...");
    const resp = await server.cut(uri);

    console.log(`Done in ${((Date.now() - start) / 1000).toFixed(3)}s`);
    return resp;
  }

  async function paste() {
    const start = Date.now();
    console.log("");
    console.log("Paste");

    console.log("> taking image...");
    // const opts = { skipProcessing: true, exif: false };
    const opts = {};
    let photo = await camera.takePictureAsync(opts);

    console.log("> resizing...");
    const { uri } = await ImageManipulator.manipulateAsync(photo.uri, [
      // { resize: { width: 512, height: 1024 } },
      { resize: { width: 350, height: 700 } },
    ]);

    console.log("> sending to /paste...");
    try {
      const resp = await server.paste(uri);
      if (resp.status !== "ok") {
        if (resp.status === "screen not found") {
          console.log("screen not found");
        } else {
          throw new Error(resp);
        }
      }
    } catch (e) {
      console.error("error pasting:", e);
    }

    console.log(`Done in ${((Date.now() - start) / 1000).toFixed(3)}s`);
  }

  async function onPressIn() {
    setPressed(true);

    const resp = await cut();

    // Check if we're still pressed.
    // if (pressed) {
    setState({ ...state, currImgSrc: resp });
    // }
  }

  async function onPressOut() {
    setPressed(false);
    setPasting(true);

    if (state.currImgSrc !== "") {
      await paste();
      setState({ ...state, currImgSrc: "" });
      setPasting(false);
    }
  }

  if (state.hasPermission === null) {
    return <View />;
  }
  if (state.hasPermission === false) {
    return <Text>No access to camera</Text>;
  }

  let camOpacity = 1;
  if (pressed && state.currImgSrc !== "") {
    camOpacity = 0.8;
  }

  return (
    <View style={{ flex: 1 }}>
      <View
        style={{ ...StyleSheet.absoluteFillObject, backgroundColor: "black" }}
      ></View>
      <Camera
        style={{ flex: 1, opacity: camOpacity }}
        type={state.type}
        ratio="2:1"
        // autoFocus={false}
        // pictureSize="640x480"
        ref={async (ref) => (camera = ref)}
      >
        <TouchableWithoutFeedback onPressIn={onPressIn} onPressOut={onPressOut}>
          <View
            style={{
              flex: 1,
              backgroundColor: "transparent",
              flexDirection: "row",
            }}
          ></View>
        </TouchableWithoutFeedback>
      </Camera>

      {pressed && state.currImgSrc !== "" ? (
        <>
          <View pointerEvents="none" style={styles.resultImgView}>
            <Image
              style={styles.resultImg}
              source={{ uri: state.currImgSrc }}
              resizeMode="stretch"
            />
          </View>
        </>
      ) : null}

      {(pressed && state.currImgSrc === "") || pasting ? <ProgressIndicator /> : null}
    </View>
  );
}


================================================
FILE: app/README.md
================================================
# AR Cut Paste Mobile App

An [Expo](expo.io) / [React Native](#) mobile application.
Please follow instructions from the [expo website](https://expo.io/learn) to see how to preview the app on your phone using the Expo app.

## Setup

```bash
npm install
```

Then update the IP address in `components/Server.tsx` to point to the IP address of the computer running the local server:
```js
3: const URL = "http://192.168.1.29:8080";
```

## Run

```bash
npm start
```


================================================
FILE: app/app.json
================================================
{
  "expo": {
    "name": "app",
    "slug": "app",
    "platforms": [
      "ios",
      "android",
      "web"
    ],
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true
    }
  }
}


================================================
FILE: app/babel.config.js
================================================
module.exports = function(api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
  };
};


================================================
FILE: app/components/Base64.tsx
================================================
// https://stackoverflow.com/questions/42829838/react-native-atob-btoa-not-working-without-remote-js-debugging
const chars =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
const Base64 = {
  btoa: (input: string = "") => {
    let str = input;
    let output = "";

    for (
      let block = 0, charCode, i = 0, map = chars;
      str.charAt(i | 0) || ((map = "="), i % 1);
      output += map.charAt(63 & (block >> (8 - (i % 1) * 8)))
    ) {
      charCode = str.charCodeAt((i += 3 / 4));

      if (charCode > 0xff) {
        throw new Error(
          "'btoa' failed: The string to be encoded contains characters outside of the Latin1 range."
        );
      }

      block = (block << 8) | charCode;
    }

    return output;
  },

  atob: (input: string = "") => {
    let str = input.replace(/=+$/, "");
    let output = "";

    if (str.length % 4 == 1) {
      throw new Error(
        "'atob' failed: The string to be decoded is not correctly encoded."
      );
    }
    for (
      let bc = 0, bs = 0, buffer, i = 0;
      (buffer = str.charAt(i++));
      ~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
        ? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
        : 0
    ) {
      buffer = chars.indexOf(buffer);
    }

    return output;
  },
};

FileReader.prototype.readAsArrayBuffer = function (blob) {
  if (this.readyState === this.LOADING) throw new Error("InvalidStateError");
  this._setReadyState(this.LOADING);
  this._result = null;
  this._error = null;
  const fr = new FileReader();
  fr.onloadend = () => {
    const content = Base64.atob(
      fr.result.substr(fr.result.indexOf(',') + 1)
    );
    const buffer = new ArrayBuffer(content.length);
    const view = new Uint8Array(buffer);
    view.set(Array.from(content).map((c) => c.charCodeAt(0)));
    this._result = buffer;
    this._setReadyState(this.DONE);
  };
  fr.readAsDataURL(blob);
};

// from: https://stackoverflow.com/questions/42829838/react-native-atob-btoa-not-working-without-remote-js-debugging
// const chars =
//   "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
// const atob = (input = "") => {
//   let str = input.replace(/=+$/, "");
//   let output = "";

//   if (str.length % 4 == 1) {
//     throw new Error(
//       "'atob' failed: The string to be decoded is not correctly encoded."
//     );
//   }
//   for (
//     let bc = 0, bs = 0, buffer, i = 0;
//     (buffer = str.charAt(i++));
//     ~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
//       ? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
//       : 0
//   ) {
//     buffer = chars.indexOf(buffer);
//   }

//   return output;
// };

export default Base64;


================================================
FILE: app/components/ProgressIndicator.tsx
================================================
// @refresh reset

import React, { useState, useEffect } from "react";
import { View, Animated, StyleSheet } from "react-native";
import Svg, { Circle } from "react-native-svg";

const AnimatedCircle = Animated.createAnimatedComponent(Circle);

const numX = 4;
const numY = 5;
const total = numX * numY;

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    alignItems: "center",
    justifyContent: "center",
  },
});
export default function ProgressIndicator() {
  const init = Array(total)
    .fill(1)
    .map((x) => ({ r: new Animated.Value(1), a: new Animated.Value(1) }));
  const [anim, setAnim] = useState(init);

  useEffect(() => {
    console.log("update");
    const c = anim.map((v, i: number) => {
      const t = 400 + Math.random() * 300;
      const seq = Animated.parallel([
        Animated.sequence([
          Animated.timing(anim[i].r, { toValue: 3, duration: t - 50 }),
          Animated.timing(anim[i].r, { toValue: 1, duration: t }),
        ]),
        Animated.sequence([
          Animated.timing(anim[i].a, { toValue: 0.1, duration: t - 50 }),
          Animated.timing(anim[i].a, { toValue: 1, duration: t }),
        ]),
      ]);
      return Animated.loop(seq);
    });
    // console.log(c)
    Animated.parallel(c).start();
  }, []);

  let circles = [];
  const margin = 100 / (numX);
  for (let x = 0; x < numX; x++) {
    for (let y = 0; y < numY; y++) {
      const i = y * numX + x;
      circles.push({
        x: (x + 0.5) * margin,
        y: (y) * margin,
        r: anim[i].r,
        a: anim[i].a,
      });
    }
  }

  return (
    <View style={styles.container}>
      <Svg height="100%" width="100%" viewBox="0 0 100 100">
        {circles.map((c) => (
          <AnimatedCircle
            key={c.y * numX + c.x}
            cx={c.x}
            cy={c.y}
            r={c.r}
            fill="white"
            opacity={c.a}
          />
        ))}
      </Svg>
    </View>
  );
}


================================================
FILE: app/components/Server.tsx
================================================
import Base64 from "./Base64";

const URL = "http://192.168.1.29:8080";

function arrayBufferToBase64(buffer: ArrayBuffer) {
  let binary = "";
  const bytes = [].slice.call(new Uint8Array(buffer));
  bytes.forEach((b) => (binary += String.fromCharCode(b)));
  return Base64.btoa(binary);
}

function ping() {
  fetch(URL + "/ping").catch((e) => console.error(e));
}

async function cut(imageURI: string) {
  const formData = new FormData();
  formData.append("data", {
    uri: imageURI,
    name: "photo",
    type: "image/jpg",
  });

  const resp = await fetch(URL + "/cut", {
    method: "POST",
    body: formData,
  }).then(async (res) => {
    console.log("> converting...");
    const buffer = await res.arrayBuffer();
    const base64Flag = "data:image/png;base64,";
    const imageStr = arrayBufferToBase64(buffer);
    return base64Flag + imageStr;
  });

  return resp;
}

async function paste(imageURI: string) {
  const formData = new FormData();
  formData.append("data", {
    uri: imageURI,
    name: "photo",
    type: "image/jpg",
  });

  const resp = await fetch(URL + "/paste", {
    method: "POST",
    body: formData,
  }).then((r) => r.json());

  return resp;
}

export default {
  ping,
  cut,
  paste,
};


================================================
FILE: app/package.json
================================================
{
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "eject": "expo eject"
  },
  "dependencies": {
    "expo": "~37.0.3",
    "expo-2d-context": "0.0.2",
    "expo-asset": "~8.1.4",
    "expo-camera": "~8.2.0",
    "expo-gl": "~8.1.0",
    "expo-image-manipulator": "~8.1.0",
    "expo-permissions": "~8.1.0",
    "mem": "^4.0.0",
    "react": "~16.9.0",
    "react-dom": "~16.9.0",
    "react-native": "https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz",
    "react-native-screens": "~2.2.0",
    "react-native-svg": "11.0.1",
    "react-native-web": "~0.11.7"
  },
  "devDependencies": {
    "@babel/core": "^7.8.6",
    "@types/react": "~16.9.23",
    "@types/react-native": "~0.61.17",
    "babel-preset-expo": "~8.1.0",
    "typescript": "~3.8.3"
  },
  "private": true
}


================================================
FILE: app/tsconfig.json
================================================
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "jsx": "react-native",
    "lib": ["dom", "esnext"],
    "moduleResolution": "node",
    "noEmit": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "strict": true
  }
}


================================================
FILE: server/README.md
================================================
# AR Cut Paste local server

## Setup

```console
virtualenv -p python3.7 venv
source venv/bin/activate
pip install -r requirements.txt
```

## Run

The `BASNET_SERVICE_HOST` is optional (only needed if you've deployed the service
on a platform using an ingress gateway such as Knative / Cloud Run).

Replace `123456` by your Photoshop remote connection password.

```console
python src/main.py \
    --basnet_service_ip="http://X.X.X.X" \
    --basnet_service_host="basnet-http.default.example.com" \
    --photoshop_password 123456
```


================================================
FILE: server/requirements.txt
================================================
screenpoint==0.1.1
photoshop-connection==0.1.1
Flask==1.1.1
flask-cors==3.0.9
pyscreenshot==1.0
Pillow==8.3.2
requests==2.23.0

================================================
FILE: server/src/main.py
================================================
import io
import os
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
from PIL import Image
import numpy as np
import time
import screenpoint
from datetime import datetime
import pyscreenshot
import requests
import logging
import argparse

import ps

logging.basicConfig(level=logging.INFO)

parser = argparse.ArgumentParser()
parser.add_argument('--photoshop_password', default='123456')
parser.add_argument('--basnet_service_ip', required=True, help="The BASNet service IP address")
parser.add_argument('--basnet_service_host', help="Optional, the BASNet service host")
args = parser.parse_args()

max_view_size = 700
max_screenshot_size = 400

# Initialize the Flask application.
app = Flask(__name__)
CORS(app)


# Simple probe.
@app.route('/', methods=['GET'])
def hello():
    return 'Hello AR Cut Paste!'

# Ping to wake up the BASNet service.
@app.route('/ping', methods=['GET'])
def ping():
    logging.info('ping')
    r = requests.get(args.basnet_service_ip, headers={'Host': args.basnet_service_host})
    logging.info(f'pong: {r.status_code} {r.content}')
    return 'pong'


# The cut endpoints performs the salience detection / background removal.
# And store a copy of the result to be pasted later.
@app.route('/cut', methods=['POST'])
def save():
    start = time.time()
    logging.info(' CUT')

    # Convert string of image data to uint8.
    if 'data' not in request.files:
        return jsonify({
            'status': 'error',
            'error': 'missing file param `data`'
        }), 400
    data = request.files['data'].read()
    if len(data) == 0:
        return jsonify({'status:': 'error', 'error': 'empty image'}), 400

    # Save debug locally.
    with open('cut_received.jpg', 'wb') as f:
        f.write(data)

    # Send to BASNet service.
    logging.info(' > sending to BASNet...')
    headers = {}
    if args.basnet_service_host is not None:
        headers['Host'] = args.basnet_service_host
    files= {'data': open('cut_received.jpg', 'rb')}
    res = requests.post(args.basnet_service_ip, headers=headers, files=files )
    # logging.info(res.status_code)

    # Save mask locally.
    logging.info(' > saving results...')
    with open('cut_mask.png', 'wb') as f:
        f.write(res.content)
        # shutil.copyfileobj(res.raw, f)

    logging.info(' > opening mask...')
    mask = Image.open('cut_mask.png').convert("L")

    # Convert string data to PIL Image.
    logging.info(' > compositing final image...')
    ref = Image.open(io.BytesIO(data))
    empty = Image.new("RGBA", ref.size, 0)
    img = Image.composite(ref, empty, mask)

    # TODO: currently hack to manually scale up the images. Ideally this would
    # be done respective to the view distance from the screen.
    img_scaled = img.resize((img.size[0] * 3, img.size[1] * 3))

    # Save locally.
    logging.info(' > saving final image...')
    img_scaled.save('cut_current.png')

    # Save to buffer
    buff = io.BytesIO()
    img.save(buff, 'PNG')
    buff.seek(0)

    # Print stats
    logging.info(f'Completed in {time.time() - start:.2f}s')

    # Return data
    return send_file(buff, mimetype='image/png')


# The paste endpoints handles new paste requests.
@app.route('/paste', methods=['POST'])
def paste():
    start = time.time()
    logging.info(' PASTE')

    # Convert string of image data to uint8.
    if 'data' not in request.files:
        return jsonify({
            'status': 'error',
            'error': 'missing file param `data`'
        }), 400
    data = request.files['data'].read()
    if len(data) == 0:
        return jsonify({'status:': 'error', 'error': 'empty image'}), 400

    # Save debug locally.
    with open('paste_received.jpg', 'wb') as f:
        f.write(data)

    # Convert string data to PIL Image.
    logging.info(' > loading image...')
    view = Image.open(io.BytesIO(data))

    # Ensure the view image size is under max_view_size.
    if view.size[0] > max_view_size or view.size[1] > max_view_size:
        view.thumbnail((max_view_size, max_view_size))

    # Take screenshot with pyscreenshot.
    logging.info(' > grabbing screenshot...')
    screen = pyscreenshot.grab()
    screen_width, screen_height = screen.size

    # Ensure screenshot is under max size.
    if screen.size[0] > max_screenshot_size or screen.size[1] > max_screenshot_size:
        screen.thumbnail((max_screenshot_size, max_screenshot_size))

    # Finds view centroid coordinates in screen space.
    logging.info(' > finding projected point...')
    view_arr = np.array(view.convert('L'))
    screen_arr = np.array(screen.convert('L'))
    # logging.info(f'{view_arr.shape}, {screen_arr.shape}')
    x, y = screenpoint.project(view_arr, screen_arr, False)

    found = x != -1 and y != -1

    if found:
        # Bring back to screen space
        x = int(x / screen.size[0] * screen_width)
        y = int(y / screen.size[1] * screen_height)
        logging.info(f'{x}, {y}')

        # Paste the current image in photoshop at these coordinates.
        logging.info(' > sending to photoshop...')
        name = datetime.today().strftime('%Y-%m-%d-%H:%M:%S')
        img_path = os.path.join(os.getcwd(), 'cut_current.png')
        err = ps.paste(img_path, name, x, y, password=args.photoshop_password)
        if err is not None:
            logging.error('error sending to photoshop')
            logging.error(err)
            jsonify({'status': 'error sending to photoshop'})
    else:
        logging.info('screen not found')

    # Print stats.
    logging.info(f'Completed in {time.time() - start:.2f}s')

    # Return status.
    if found:
        return jsonify({'status': 'ok'})
    else:
        return jsonify({'status': 'screen not found'})


if __name__ == '__main__':
    os.environ['FLASK_ENV'] = 'development'
    port = int(os.environ.get('PORT', 8080))
    app.run(debug=True, host='0.0.0.0', port=port)


================================================
FILE: server/src/ps.py
================================================
from photoshop import PhotoshopConnection
from os.path import dirname, basename

# TODO: This offset should be detected by getTopLeft() but the new version
# of Photoshop doesn't seem to support executeActionGet so we put it
# manually here in the meantime.
SCREEN_PIXELS_DENSITY = 2
DOC_OFFSET_X = 74 * SCREEN_PIXELS_DENSITY
DOC_OFFSET_Y = 130 * SCREEN_PIXELS_DENSITY
DOC_WIDTH = 2121
DOC_HEIGHT = 1280

def paste(filename, name, x, y, password='123456'):

    # There seem to be a bug on Windows where the path must be using unix separators.
    # https://github.com/cyrildiagne/ar-cutpaste/issues/5
    filename = filename.replace('\\', '/')

    with PhotoshopConnection(password=password) as conn:
        script = open(basename(dirname(__file__)) + '/script.js', 'r').read()
        x -= DOC_WIDTH * 0.5 + DOC_OFFSET_X
        y -= DOC_HEIGHT * 0.5 + DOC_OFFSET_Y
        script += f'pasteImage("{filename}", "{name}", {x}, {y})'
        result = conn.execute(script)
        print(result)
        if result['status'] != 0:
            return result
    
    return None


================================================
FILE: server/src/script.js
================================================
function pasteImage(filename, layerName, x, y) {
  var fileRef = new File(filename);
  var doc = app.activeDocument;

  doc.artLayers.add();
  var curr_file = app.open(fileRef);
  curr_file.selection.selectAll();
  curr_file.selection.copy();
  curr_file.close();

  doc.paste();
  doc.activeLayer.name = layerName;
  doc.activeLayer.translate(x, y);
  try {
    doc.activeLayer.move(doc.layers[doc.layers.length - 1], ElementPlacement.PLACEBEFORE);
  } catch(e) {
    alert(e);
  }
}

function getTopLeft() {
  try {
    var r = new ActionReference();
    executeActionGet(r)
      .getObjectValue(stringIDToTypeID("viewInfo"))
      .getObjectValue(stringIDToTypeID("activeView"))
      .getObjectValue(stringIDToTypeID("globalBounds"));
    alert(t)
  } catch (e) {
    alert(e);
  }
}

Download .txt
gitextract_7t568dfp/

├── .gitignore
├── LICENSE
├── README.md
├── app/
│   ├── .expo-shared/
│   │   └── assets.json
│   ├── .gitignore
│   ├── App.tsx
│   ├── README.md
│   ├── app.json
│   ├── babel.config.js
│   ├── components/
│   │   ├── Base64.tsx
│   │   ├── ProgressIndicator.tsx
│   │   └── Server.tsx
│   ├── package.json
│   └── tsconfig.json
└── server/
    ├── README.md
    ├── requirements.txt
    └── src/
        ├── main.py
        ├── ps.py
        └── script.js
Download .txt
SYMBOL INDEX (15 symbols across 6 files)

FILE: app/App.tsx
  type State (line 34) | interface State {
  function App (line 41) | function App() {

FILE: app/components/ProgressIndicator.tsx
  function ProgressIndicator (line 20) | function ProgressIndicator() {

FILE: app/components/Server.tsx
  constant URL (line 3) | const URL = "http://192.168.1.29:8080";
  function arrayBufferToBase64 (line 5) | function arrayBufferToBase64(buffer: ArrayBuffer) {
  function ping (line 12) | function ping() {
  function cut (line 16) | async function cut(imageURI: string) {
  function paste (line 38) | async function paste(imageURI: string) {

FILE: server/src/main.py
  function hello (line 35) | def hello():
  function ping (line 40) | def ping():
  function save (line 50) | def save():
  function paste (line 114) | def paste():

FILE: server/src/ps.py
  function paste (line 13) | def paste(filename, name, x, y, password='123456'):

FILE: server/src/script.js
  function pasteImage (line 1) | function pasteImage(filename, layerName, x, y) {
  function getTopLeft (line 21) | function getTopLeft() {
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (30K chars).
[
  {
    "path": ".gitignore",
    "chars": 84,
    "preview": "venv\n.vscode\n.DS_Store\n.pyc\nnode_modules\n__pycache__\nserver/*.png\nserver/*.jpg\n*.psd"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2020 Cyril Diagne\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 3918,
    "preview": "# AR Cut & Paste\n\nAn AR+ML prototype that allows cutting elements from your surroundings and pasting them in an image ed"
  },
  {
    "path": "app/.expo-shared/assets.json",
    "chars": 155,
    "preview": "{\n  \"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb\": true,\n  \"40b842e832070c58deac6aa9e08fa459302ee3f"
  },
  {
    "path": "app/.gitignore",
    "chars": 130,
    "preview": "node_modules/**/*\n.expo/*\nnpm-debug.*\n*.jks\n*.p8\n*.p12\n*.key\n*.mobileprovision\n*.orig.*\nweb-build/\nweb-report/\n\n# macOS\n"
  },
  {
    "path": "app/App.tsx",
    "chars": 5400,
    "preview": "import React, { useState, useEffect } from \"react\";\nimport {\n  Text,\n  View,\n  Image,\n  TouchableWithoutFeedback,\n  Styl"
  },
  {
    "path": "app/README.md",
    "chars": 467,
    "preview": "# AR Cut Paste Mobile App\n\nAn [Expo](expo.io) / [React Native](#) mobile application.\nPlease follow instructions from th"
  },
  {
    "path": "app/app.json",
    "chars": 496,
    "preview": "{\n  \"expo\": {\n    \"name\": \"app\",\n    \"slug\": \"app\",\n    \"platforms\": [\n      \"ios\",\n      \"android\",\n      \"web\"\n    ],\n"
  },
  {
    "path": "app/babel.config.js",
    "chars": 107,
    "preview": "module.exports = function(api) {\n  api.cache(true);\n  return {\n    presets: ['babel-preset-expo'],\n  };\n};\n"
  },
  {
    "path": "app/components/Base64.tsx",
    "chars": 2756,
    "preview": "// https://stackoverflow.com/questions/42829838/react-native-atob-btoa-not-working-without-remote-js-debugging\nconst cha"
  },
  {
    "path": "app/components/ProgressIndicator.tsx",
    "chars": 1976,
    "preview": "// @refresh reset\n\nimport React, { useState, useEffect } from \"react\";\nimport { View, Animated, StyleSheet } from \"react"
  },
  {
    "path": "app/components/Server.tsx",
    "chars": 1234,
    "preview": "import Base64 from \"./Base64\";\n\nconst URL = \"http://192.168.1.29:8080\";\n\nfunction arrayBufferToBase64(buffer: ArrayBuffe"
  },
  {
    "path": "app/package.json",
    "chars": 930,
    "preview": "{\n  \"main\": \"node_modules/expo/AppEntry.js\",\n  \"scripts\": {\n    \"start\": \"expo start\",\n    \"android\": \"expo start --andr"
  },
  {
    "path": "app/tsconfig.json",
    "chars": 258,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"jsx\": \"react-native\",\n    \"lib\": [\"dom\", \"esnext"
  },
  {
    "path": "server/README.md",
    "chars": 538,
    "preview": "# AR Cut Paste local server\n\n## Setup\n\n```console\nvirtualenv -p python3.7 venv\nsource venv/bin/activate\npip install -r r"
  },
  {
    "path": "server/requirements.txt",
    "chars": 126,
    "preview": "screenpoint==0.1.1\nphotoshop-connection==0.1.1\nFlask==1.1.1\nflask-cors==3.0.9\npyscreenshot==1.0\nPillow==8.3.2\nrequests=="
  },
  {
    "path": "server/src/main.py",
    "chars": 5915,
    "preview": "import io\nimport os\nfrom flask import Flask, request, jsonify, send_file\nfrom flask_cors import CORS\nfrom PIL import Ima"
  },
  {
    "path": "server/src/ps.py",
    "chars": 1077,
    "preview": "from photoshop import PhotoshopConnection\nfrom os.path import dirname, basename\n\n# TODO: This offset should be detected "
  },
  {
    "path": "server/src/script.js",
    "chars": 790,
    "preview": "function pasteImage(filename, layerName, x, y) {\n  var fileRef = new File(filename);\n  var doc = app.activeDocument;\n\n  "
  }
]

About this extraction

This page contains the full source code of the cyrildiagne/ar-cutpaste GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (26.8 KB), approximately 7.8k tokens, and a symbol index with 15 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!