designsystemsinternational / mechanic

Mechanic is a framework to build assets built on web code.
https://mechanic.design
MIT License
253 stars 11 forks source link

Image Upload with the react engine won't work #130

Closed kunaumadein closed 2 years ago

kunaumadein commented 2 years ago

Describe the bug To get a feel for the framework I had the Business Card Generator compiled as an example. I wanted to test something basic and add text and 2-3 images. But when I add an upload image input and reference it in react via the image is displayed as broken.

Expected behavior the uploaded Image should work as a background on the whole canvas.

Desktop (please complete the following information):

kunaumadein commented 2 years ago
import React, { useEffect } from "react";
import "./styles.css";

export const handler = ({ inputs, mechanic }) => {
  const {
    width,
    height,
    backgroundColor,
    myImage,
    colorOne,
    colorTwo,
    fontScale,
    company,
    name,
    email,
    email2,
    phone,
  } = inputs;

  const margin = width / 18;
  const fontSize = (width / 18) * fontScale;
  const textTop = margin + fontSize * 0.7;
  const lineHeight = fontSize * 1.2;

  useEffect(() => {
    mechanic.done();
  }, []);

  return (
    <svg width={width} height={height}>
      <rect fill={backgroundColor} width={width} height={height} />
        {/* Bildimplementierung via Upload */}
        <image href={myImage} width={1200} height={627} />
        {/* Bildimplementierung via URl */}
        {/*
        <image href={"https://i.imgur.com/zUeB6rK.png"} />
        */}

      {/* The text */}
      <text
        x={25}
        y={75}
        fill={colorOne}
        textAnchor="start"
        fontWeight="regular"
        fontFamily="Garnett Regular"
        fontSize={30}
      >
        {name}
      </text>

      <text
        x={25}
        y={300}
        fill={colorOne}
        textAnchor="start"
        fontWeight="medium"
        fontFamily="Garnett Medium"
        fontSize={72}
      >
        {email}
      </text>

      <text
        x={25}
        y={385}
        fill={colorOne}
        textAnchor="start"
        fontWeight="medium"
        fontFamily="Garnett Medium"
        fontSize={72}
      >
        {email2}
      </text>

      <text
        x={25}
        y={575}
        fill={colorOne}
        textAnchor="start"
        fontWeight="regular"
        fontFamily="Garnett Regular"
        fontSize={48}
      >
        {phone}
      </text>

      {/* To align something to the right, use the width minus the margin*/}

      {/*
      <text
        x={width - margin}
        y={height - margin}
        fill={colorTwo}
        textAnchor="end"
        fontWeight="regular"
        fontFamily="Garnett Regular"
        fontSize={fontSize}
      >
        {company}
      </text>
    */}
    </svg>
  );
};

export const inputs = {
  width: {
    type: "number",
    default: 850,
  },
  height: {
    type: "number",
    default: 550,
  },
  backgroundColor: {
    type: "color",
    model: "hex",
    default: "#FDD7D1",
  },
  myImage: { 
    type: "image",
    multiple: false,
  },
  colorOne: {
    type: "color",
    model: "hex",
    default: "#E94225",
  },
  colorTwo: {
    type: "color",
    model: "hex",
    default: "#002EBB",
  },
  fontScale: {
    type: "number",
    default: 1,
    slider: true,
    min: 0.1,
    max: 3,
    step: 0.01,
  },
  company: {
    type: "text",
    default: "MECHANIC",
  },
  name: {
    type: "text",
    default: "Martin Bravo",
  },
  email: {
    type: "text",
    default: "martin@mechanic.design",
  },
  email2: {
    type: "text",
    default: "martin@mechanic.design",
  },
  phone: {
    type: "text",
    default: "+1 999 999 9999",
  },
};

//Voreinstellungen/Presets
export const presets = {
  medium: {
    width: 800,
    height: 600,
  },
  large: {
    width: 1600,
    height: 1200,
  },
  LinkedIn: {
    width: 1200,
    height: 627,
  },
};

export const settings = {
  engine: require("@mechanic-design/engine-react"),
};
fdoflorenzano commented 2 years ago

Hi @kunaumadein ! Sorry for the delay answering.

The image input it's a tricky one right now. The value it returns to the design function isn't something that can be used that directly, it's actually a File Object as that's the type of object that an HTML file input returns.

There are ways of using that object though. In your case, for a React based function, you would need something like this:

export const handler = ({ inputs, mechanic }) => {
  const {
    myImage,
  } = inputs;
  const [href, setHref] = useState("");

  useEffect(() => {
    if (myImage) {
      const reader = new FileReader();

      reader.onload = function () {
        setHref(reader.result);
      };

      reader.readAsDataURL(myImage);
    }
  }, []);

  return  <image href={href} />;

This is not the best, and we are aware of it, hopefully we refactor how this input works in the future. Although it's already possible to define your own input components, so maybe someone does it before!

I hope this helps!

kunaumadein commented 2 years ago

Thank you for the answer @fdoflorenzano Ok I understand and I have implented the input function in my code and removed the inout definition above accordingly. But I still get an error message and that is useState("") is not defined. Currently i implemented this way:

import React, { useEffect } from "react";
import "./styles.css";

export const handler = ({ inputs, mechanic }) => {
  const {
    width,
    height,
    backgroundColor,
    colorOne,
    colorTwo,
    fontScale,
    company,
    name,
    email,
    email2,
    phone,
  } = inputs;

  const margin = width / 18;
  const fontSize = (width / 18) * fontScale;
  const textTop = margin + fontSize * 0.7;
  const lineHeight = fontSize * 1.2;

  const {
    myImage,
  } = inputs;
  const [href, setHref] = useState("");

  useEffect(() => {
    if (myImage) {
      const reader = new FileReader();

      reader.onload = function () {
        setHref(reader.result);
      };

      reader.readAsDataURL(myImage);
    }
  }, []);

  useEffect(() => {
    mechanic.done();
  }, []);

Thank you in advance!

Bildschirmfoto 2022-05-09 um 09 15 04
kunaumadein commented 2 years ago

EDIT: I needed to import useState here:

import React, { useEffect, useState } from "react";
import "./styles.css";

Now it is working!

fdoflorenzano commented 2 years ago

Amazing! 🎉

kunaumadein commented 2 years ago

Hello it is me again! I was just curious it it's possible to implement two image upload functions or is it currently limited to one image upload somehow? I wanted to import a logo in the top right corner and a logo in the bottom left corner.

lucasdinonolte commented 2 years ago

@kunaumadein sure, using the technique described by @fdoflorenzano you can add as many images as you'd like.

import React, { useEffect, useState } from "react";

export const handler = ({ inputs, mechanic }) => {
  const { topLogo, bottomLogo } = inputs;
  const [topHref, setTopHref] = useState("");
  const [bottomHref, setBottomHref] = useState("");

  const loadImageFromFileObject = (fileObject, stateSetter) => {
    const reader = new FileReader();
    reader.onload = () => {
      stateSetter(reader.result);
    };
    reader.readAsDataURL(fileObject);
  };

  useEffect(() => {
    if (topLogo) loadImageFromFileObject(topLogo, setTopHref);
    if (bottomLogo) loadImageFromFileObject(bottomLogo, setBottomHref);

    mechanic.done();
  }, []);

  return (
    <svg width={400} height={400}>
      <image href={topHref} />
      <image href={bottomHref} />
    </svg>
  );
};

export const inputs = {
  topLogo: {
    type: "image",
    multiple: false,
  },
  bottomLogo: {
    type: "image",
    multiple: false,
  },
};

export const settings = {
  engine: require("@mechanic-design/engine-react"),
};
kunaumadein commented 2 years ago

ahh that's great @lnolte @fdoflorenzano to rename the "hrefs" I also tried earlier but your variant is much better! Currently I have also built in a multiple export option so that I can output it as SVG or as PNG but it happens in the export that my 2 images that I retrieve through the filereader then do not appear in the PNG export. Only the background that I have currently imported and linked locally is displayed in the PNG export. Do I have to save the images in the browser storage from the retrieved filereader in between or something?

my current background which I imported from my machine:

import React, { useEffect, useState } from "react";
import ImgBanner from './assets/banner.svg';
import "./styles.css";

i return everything via:

<image href={topHref}>
<image href={bottomHref}>

And i defined my inputs like the code you mentioned above. And after I hit export as PNG these to images will flicker for like 1 second and my final export is without the imported images.

EDIT: I changed the current position of mechanic done(); into this and it will download multiple PNG files and sometimes i will get a PNG with the topLogo or a PNG with the bottomLogo

mechanic.done();
    useEffect(() => {
      if (topLogo) loadImageFromFileObject(topLogo, setTopHref);
      if (bottomLogo) loadImageFromFileObject(bottomLogo, setBottomHref);
  }, []);
lucasdinonolte commented 2 years ago

Interesting. So the way you've currently set it up mechanic.done() will be called everytime the component re-renders. Once an image is loaded this triggers a re-render, so that's why it's multiple downloads.

Normally state (where you store the hrefs) should be persisted between renders. So if it's working in the preview it should in theory also yield a working download. But exporting PNGs from SVG functions is a relatively new feature and I'm not fully aware of how it's implemented. I'll have a look at it later and get back to you :-)

kunaumadein commented 2 years ago

My solution with mechanic.done() is not final i was just curious on how it would behave. Importing different image types is of course working and stuff I am just confused why it won't download the uploaded images into my final PNG.

The method above where i put the useEffect(); into the mechanic.done(); behaves like that, that my first image that i upload will be downloaded into my final PNG file. If i trigger the second upload option then it will render that image in my final PNG. So it is working but it will download 2 PNG files where one of them is without the uploaded images.

But thank you @lnolte for having a look :)

EDIT: just in case my current code from the index.js file

import React, { useEffect, useState } from "react";
import ImgBanner from './assets/banner.svg';
import "./styles.css";

export const handler = ({ inputs, mechanic }) => {
  const {
    subtitel,
    subtitel2,
    titel,
    titel2,
    untertitel,
    sponsoring,
  } = inputs;

  const { topLogo, bottomLogo } = inputs;
  const [topHref, setTopHref] = useState("");
  const [bottomHref, setBottomHref] = useState("");

  const loadImageFromFileObject = (fileObject, stateSetter) => {
    const reader = new FileReader();
    reader.onload = () => {
      stateSetter(reader.result);
    };
    reader.readAsDataURL(fileObject);
  };

  useEffect(() => {
    if (topLogo) loadImageFromFileObject(topLogo, setTopHref);
    if (bottomLogo) loadImageFromFileObject(bottomLogo, setBottomHref);

    mechanic.done();
  }, []);

  return (
    <svg width={1200} height={627}>
      <rect width={1200} height={627} />
        <image href={ImgBanner} width={1200} height={627} />
        <image
          x={950}
          y={-10}
          width={200}
          height={200}
          href={topHref}
        >
        </image>

        <image
          x={200}
          y={447}
          width={230}
          height={230}
          href={bottomHref}
        >
        </image>
        {/* The text */}

        <text
          x={25}
          y={75}
          fill={'#ffffff'}
          textAnchor="start"
          fontWeight="regular"
          fontFamily="Garnett Regular"
          fontSize={30}
        >
          {subtitel}
        </text>

        <text
          x={25}
          y={125}
          fill={'#ffffff'}
          textAnchor="start"
          fontWeight="regular"
          fontFamily="Garnett Regular"
          fontSize={30}
        >
          {subtitel2}
        </text>

        <text
          x={25}
          y={300}
          fill={'#ffffff'}
          textAnchor="start"
          fontWeight="regular"
          fontFamily="Garnett Regular"
          fontSize={72}
        >
          {titel}
          <tspan>

          </tspan>
        </text>

        <text
          x={25}
          y={385}
          fill={'#ffffff'}
          textAnchor="start"
          fontWeight="regular"
          fontFamily="Garnett Regular"
          fontSize={72}
        >
          {titel2}
        </text>

        <text
          x={25}
          y={575}
          fill={'#ffffff'}
          textAnchor="start"
          fontWeight="regular"
          fontFamily="Garnett Regular"
          fontSize={30}
        >
          {untertitel}
        </text>

        <text
          x={25}
          y={575}
          fill={'#ffffff'}
          textAnchor="start"
          fontWeight="regular"
          fontFamily="Garnett Regular"
          fontSize={20}
        >
          {sponsoring}
        </text>

    </svg>
  );
};

export const inputs = {
  topLogo: { 
    type: "image",
    multiple: false,
  },
  bottomLogo: { 
    type: "image",
    multiple: false,
  },
  subtitel: {
    type: "text",
    default: "Monday March 21st: From 11.00–11.45",
  },
  subtitel2: {
    type: "text",
    default: "Webinar",
  },
  titel: {
    type: "text",
    default: "Guidelines for Blockchain",
  },
  titel2: {
    type: "text",
    default: "and DLT Governance",
  },
  untertitel: {
    type: "text",
    default: "– the new standard explained",
  },
  sponsoring: {
    type: "text",
    default: "",
  },
};

//Voreinstellungen/Presets
export const presets = {
  LinkedIn: {
    width: 1200,
    height: 627,
  },
};

export const settings = {
  engine: require("@mechanic-design/engine-react"),
  showMultipleExports: true,
  hideScaleToFit: true,
  hideGenerate: true,
};
lucasdinonolte commented 2 years ago

@kunaumadein sorry it took a bit to get to this. I think there is some work for us to do, to make working with image uploads more straightforward. Thanks again so much for pointing this out 😊

For now you should be able to get things working as expected by wrapping mechanic.done() in it's own useEffect with the loaded images being the dependencies.

import React, { useEffect, useState } from "react";

export const handler = ({ inputs, mechanic }) => {
  const { topLogo, bottomLogo } = inputs;
  const [topHref, setTopHref] = useState("");
  const [bottomHref, setBottomHref] = useState("");

  const loadImageFromFileObject = (fileObject, stateSetter) => {
    const reader = new FileReader();
    reader.onload = () => {
      stateSetter(reader.result);
    };
    reader.readAsDataURL(fileObject);
  };

  useEffect(() => {
    if (topLogo) loadImageFromFileObject(topLogo, setTopHref);
    if (bottomLogo) loadImageFromFileObject(bottomLogo, setBottomHref);
  }, []);

  useEffect(() => {
    if (topHref && bottomHref) mechanic.done();
  }, [topHref, bottomHref]);

  return (
    <svg width={400} height={400}>
      <rect x={0} y={0} width={400} height={400} fill="black" />
      <image href={topHref} />
      <image href={bottomHref} />
    </svg>
  );
};

export const inputs = {
  topLogo: {
    type: "image",
    multiple: false,
  },
  bottomLogo: {
    type: "image",
    multiple: false,
  },
};

export const settings = {
  engine: require("@mechanic-design/engine-react"),
  showMultipleExports: true,
  optimize: false,
};

This will evaluate the second useEffect every time topHref or bottomHref changes, but only call mechanic.done if both have a value (so if both images have loaded).

kunaumadein commented 2 years ago

Hello @lnolte I just implemented everything in my code and it works now! In each case the export of PNG files and SVG files is working fine right now.

My last question would be if the engine was updated somehow because i wanted to npm run build my project but somehow i will get the error message that my getStaticPath is not defined. But all the other commands will work like npm run serve for example. And the first projects/examples that i imported via terminal will build without any problems. I installed all my dependencies via npm and not yarn.

Thank you!

Bildschirmfoto 2022-05-16 um 12 35 54
fdoflorenzano commented 2 years ago

@kunaumadein That error could be from a update we did in a recent release. Please report that last error in another issue, since it looks like another error not related to the original one reported in this issue. Make sure to check and report which version of mechanic you are using.

kunaumadein commented 2 years ago

@fdoflorenzano I see ok I will report it in another issue. And thanks again for al the support with the image upload function!

kunaumadein commented 2 years ago

Hello @lnolte, I tried to implement a little workaround and wanted to make sure, that i can upload 2 images or only 1 image and then export everything. Is it possible to say that I have my 2 image upload options but in a special use case the client will only upload one image and then wants to export the whole project with that single image he uploaded.

I tried to make something work with logical operators but everytime the mechanic.done(); function will be recalled several times and i will sometimes download 2 or 3 PNG files.

fdoflorenzano commented 2 years ago

@kunaumadein do you mind sharing the section of code that tries this? Shoudn't it be something like:

useEffect(() => {
    if (topHref || bottomHref) mechanic.done();
  }, [topHref, bottomHref]);

?

kunaumadein commented 2 years ago

@fdoflorenzano oh yeah sorry

My Code:

import React, { useEffect, useState } from "react";
import ImgBanner from './assets/banner.svg';
import "./styles.css";

export const handler = ({ inputs, mechanic }) => {
  const {
    subtitel,
    subtitel2,
    titel,
    titel2,
    untertitel,
    sponsoring,
  } = inputs;

  const { topLogo, bottomLogo } = inputs;
  const [topHref, setTopHref] = useState("");
  const [bottomHref, setBottomHref] = useState("");

  const loadImageFromFileObject = (fileObject, stateSetter) => {
    const reader = new FileReader();
    reader.onload = () => {
      stateSetter(reader.result);
    };
    reader.readAsDataURL(fileObject);
  };

  useEffect(() => {
    if (topLogo) loadImageFromFileObject(topLogo, setTopHref);
    if (bottomLogo) loadImageFromFileObject(bottomLogo, setBottomHref);
  }, []);

  useEffect(() => {
    if (topHref || bottomHref) mechanic.done();
 }, [topHref, bottomHref]);

The method with if (topHref || bottomHref) mechanic.done(); i tried it before and when i import only one image i will work. But then lets assume i will upload 2 images then it will first download the PNG file with the image your uploaded first and then it will download the other PNG file with the 2 uploaded images.

fdoflorenzano commented 2 years ago

Gotcha, I was missing the rest of it. You get that because the readers change the state of the React component in different moments, so you get two calls to mechanic.done.

In a way, you need an extra validation to check that if there's an image waiting to be loaded, the function waits for it to load and to call mechanic.done. Maybe something like this is what you need:

useEffect(() => {
    if ((topHref || bottomHref) && (!topLogo ||  topHref) && (!bottomLogo || bottomHref)) mechanic.done();
 }, [topLogo, topHref, bottomLogo,  bottomHref]);

Also @kunaumadein, at this point these questions are becoming more about React rendering more than about Mechanic. Please consider researching about React state and rendering, that could probably help you with some of these questions.

kunaumadein commented 2 years ago

I just tried your code and everything works! Thank you again @fdoflorenzano

And also realized that my questions are going more and more in the direction of react but my problem with the images you have solved wonderfully and therefore you could close the ticket at this point :)