Merge pull request #1439 from pnp/hb-photo-editor-2

Hb photo editor 2
This commit is contained in:
Hugo Bernier 2020-08-12 23:27:57 -04:00 committed by GitHub
commit 40930889f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 119 additions and 104 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "smart-profile-photo-editor", "name": "smart-profile-photo-editor",
"version": "0.0.1", "version": "1.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -2487,11 +2487,6 @@
"isomorphic-fetch": "^2.2.1" "isomorphic-fetch": "^2.2.1"
} }
}, },
"@microsoft/microsoft-graph-types": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-1.7.0.tgz",
"integrity": "sha512-Mxu5H+69F8T5NzV4+U8FkTvpIYYWHsmRZzfAuOlIO0zJJGlVyRIVqpq4NmOdUXGC00vZ73ONgCuzuaksxqDm/Q=="
},
"@microsoft/node-core-library": { "@microsoft/node-core-library": {
"version": "3.13.0", "version": "3.13.0",
"resolved": "https://registry.npmjs.org/@microsoft/node-core-library/-/node-core-library-3.13.0.tgz", "resolved": "https://registry.npmjs.org/@microsoft/node-core-library/-/node-core-library-3.13.0.tgz",
@ -4264,22 +4259,6 @@
} }
} }
}, },
"@pnp/graph": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@pnp/graph/-/graph-1.3.8.tgz",
"integrity": "sha512-uBBDrpWNILGBfTxtHqFrdv3YkJZm+jgXOLBp3KR8ocBrTt+yQmkCEwztawYqUhSys6SoDkcFE/DqLIjhyHiBDA==",
"requires": {
"@microsoft/microsoft-graph-types": "1.7.0",
"tslib": "1.10.0"
},
"dependencies": {
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
}
}
},
"@pnp/logging": { "@pnp/logging": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/@pnp/logging/-/logging-1.3.8.tgz", "resolved": "https://registry.npmjs.org/@pnp/logging/-/logging-1.3.8.tgz",
@ -4310,21 +4289,6 @@
} }
} }
}, },
"@pnp/sp": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@pnp/sp/-/sp-1.3.8.tgz",
"integrity": "sha512-x85cQL/L5fBYJWqWvDL3e2sHdYZIqUeWifFndsGRk6iLFHTq2DsxNNzTnCpP7JCKUwK6jHpu0JzIuK98E8Hl9w==",
"requires": {
"tslib": "1.10.0"
},
"dependencies": {
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
}
}
},
"@pnp/sp-clientsvc": { "@pnp/sp-clientsvc": {
"version": "1.3.11", "version": "1.3.11",
"resolved": "https://registry.npmjs.org/@pnp/sp-clientsvc/-/sp-clientsvc-1.3.11.tgz", "resolved": "https://registry.npmjs.org/@pnp/sp-clientsvc/-/sp-clientsvc-1.3.11.tgz",
@ -4487,6 +4451,21 @@
"react-ace": "5.8.0" "react-ace": "5.8.0"
}, },
"dependencies": { "dependencies": {
"@pnp/sp": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/@pnp/sp/-/sp-1.3.11.tgz",
"integrity": "sha512-NjdeGe81aukiSPelSPjgAFRC1+SrNPTXvTdEqTH+Q1ZvgNtk8bdZp6K6xf9emfeM2qZDOu9GpKZpg0W/emq++g==",
"requires": {
"tslib": "1.10.0"
},
"dependencies": {
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
}
}
},
"@uifabric/icons": { "@uifabric/icons": {
"version": "5.8.0", "version": "5.8.0",
"resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-5.8.0.tgz", "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-5.8.0.tgz",

View File

@ -20,11 +20,6 @@
"@microsoft/sp-office-ui-fabric-core": "1.11.0", "@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0", "@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0", "@microsoft/sp-webpart-base": "1.11.0",
"@pnp/common": "^1.3.8",
"@pnp/graph": "^1.3.8",
"@pnp/logging": "^1.3.8",
"@pnp/odata": "^1.3.8",
"@pnp/sp": "^1.3.8",
"@pnp/spfx-controls-react": "^1.19.0", "@pnp/spfx-controls-react": "^1.19.0",
"@pnp/spfx-property-controls": "^1.20.0-beta.1472053", "@pnp/spfx-property-controls": "^1.20.0-beta.1472053",
"cropperjs": "^1.5.6", "cropperjs": "^1.5.6",

View File

@ -23,9 +23,11 @@ export class AnalysisService implements IAnalysisService {
new ApiKeyCredentials({ inHeader: { 'Ocp-Apim-Subscription-Key': this.key } }), this.endpoint); new ApiKeyCredentials({ inHeader: { 'Ocp-Apim-Subscription-Key': this.key } }), this.endpoint);
var analysis: AnalyzeImageInStreamResponse = (await computerVisionClient.analyzeImageInStream(buf, { var analysis: AnalyzeImageInStreamResponse = (await computerVisionClient.analyzeImageInStream(buf, {
details: ["Celebrities"],
visualFeatures: ["Categories", visualFeatures: ["Categories",
"Adult", "Adult",
"Tags", "Tags",
"Tags",
"Description", "Description",
"Faces", "Faces",
"Color", "Color",

View File

@ -3,41 +3,41 @@ import * as React from 'react';
import { IAnalysisDialogContentProps } from './IAnalysisDialogContentProps'; import { IAnalysisDialogContentProps } from './IAnalysisDialogContentProps';
import { IAnalysisDialogContentState } from './IAnalysisDialogContentState'; import { IAnalysisDialogContentState } from './IAnalysisDialogContentState';
import styles from './AnalysisDialogContent.module.scss';
import { css } from "@uifabric/utilities/lib/css";
// Used for localized text
import * as strings from 'ProfilePhotoEditorWebPartStrings';
import { Text } from '@microsoft/sp-core-library';
// Used to determine if we should be making real calls to APIs or just mock calls
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
// Stuff we use for the dialog
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import {
MessageBar,
MessageBarType
} from 'office-ui-fabric-react';
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
import { Panel } from 'office-ui-fabric-react/lib/Panel'; import { Panel } from 'office-ui-fabric-react/lib/Panel';
import { Label } from 'office-ui-fabric-react/lib/Label'; import { Label } from 'office-ui-fabric-react/lib/Label';
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer'; import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator'; import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
import styles from './AnalysisDialogContent.module.scss';
// Used for localized text // Stuff we use for analysis results
import * as strings from 'ProfilePhotoEditorWebPartStrings';
// Used to determine if we should be making real calls to APIs or just mock calls
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { css } from "@uifabric/utilities/lib/css";
import {
MessageBar,
MessageBarType
} from 'office-ui-fabric-react';
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
import { IAnalysisService, AnalysisService, MockAnalysisService } from '../../../../services/AnalysisServices'; import { IAnalysisService, AnalysisService, MockAnalysisService } from '../../../../services/AnalysisServices';
import { AnalyzeImageInStreamResponse, ImageTag } from '@azure/cognitiveservices-computervision/esm/models'; import { AnalyzeImageInStreamResponse, ImageTag } from '@azure/cognitiveservices-computervision/esm/models';
import AnalysisChecklist from '../AnalysisChecklist/AnalysisChecklist'; import AnalysisChecklist from '../AnalysisChecklist/AnalysisChecklist';
import { sp } from "@pnp/sp"; // This is used if you use the graph client to update pictures
import { MSGraphClient, SPHttpClient } from '@microsoft/sp-http'; import { MSGraphClient } from '@microsoft/sp-http';
export class AnalysisDialogContent extends export class AnalysisDialogContent extends
React.Component<IAnalysisDialogContentProps, IAnalysisDialogContentState> { React.Component<IAnalysisDialogContentProps, IAnalysisDialogContentState> {
/**
*
*/
constructor(props: IAnalysisDialogContentProps) { constructor(props: IAnalysisDialogContentProps) {
super(props); super(props);
this.state = { this.state = {
@ -70,11 +70,23 @@ export class AnalysisDialogContent extends
const analysis: AnalyzeImageInStreamResponse = await service.AnalyzeImage(this.props.imageUrl); const analysis: AnalyzeImageInStreamResponse = await service.AnalyzeImage(this.props.imageUrl);
// Evaluate analysis against requirements // Evaluate analysis against requirements
// Is this a portrait?
const isPortrait: boolean = analysis && analysis.categories && analysis.categories.filter(c => c.name === "people_portrait").length > 0; const isPortrait: boolean = analysis && analysis.categories && analysis.categories.filter(c => c.name === "people_portrait").length > 0;
// If the portrait valid?
const isPortraitValid: boolean = photoRequirements.requirePortrait ? isPortrait : true; const isPortraitValid: boolean = photoRequirements.requirePortrait ? isPortrait : true;
// is there only one person in the photo?
const onlyOnePersonValid: boolean = analysis.faces.length === 1; const onlyOnePersonValid: boolean = analysis.faces.length === 1;
// Is this a clipart?
const isClipartValid: boolean = photoRequirements.allowClipart ? true : analysis.imageType.clipArtType === 0; const isClipartValid: boolean = photoRequirements.allowClipart ? true : analysis.imageType.clipArtType === 0;
// Is this a line drawing?
const isLinedrawingValid: boolean = photoRequirements.allowLinedrawing ? true : analysis.imageType.lineDrawingType === 0; const isLinedrawingValid: boolean = photoRequirements.allowLinedrawing ? true : analysis.imageType.lineDrawingType === 0;
// Are we looking at naughty pictures?
const isAdultValid: boolean = photoRequirements.allowAdult ? true : !analysis.adult.isAdultContent; const isAdultValid: boolean = photoRequirements.allowAdult ? true : !analysis.adult.isAdultContent;
const isRacyValid: boolean = photoRequirements.allowRacy ? true : !analysis.adult.isRacyContent; const isRacyValid: boolean = photoRequirements.allowRacy ? true : !analysis.adult.isRacyContent;
const isGoryValid: boolean = photoRequirements.allowGory ? true : !analysis.adult.isGoryContent; const isGoryValid: boolean = photoRequirements.allowGory ? true : !analysis.adult.isGoryContent;
@ -91,9 +103,16 @@ export class AnalysisDialogContent extends
}); });
} }
// Did we find forbidden keywords
const keywordsValid: boolean = invalidKeywords.length < 1; const keywordsValid: boolean = invalidKeywords.length < 1;
console.log("Invalid keywords", invalidKeywords); // Look for celebrities
let celebName: string = undefined;
const categories = analysis && analysis.categories && analysis.categories.filter(c => c.detail !== undefined && c.detail.celebrities !== undefined);
if (categories && categories.length > 0) {
// Get the first celebrity
celebName = categories[0] && categories[0].detail && categories[0].detail.celebrities[0] && categories[0].detail.celebrities[0].name;
}
// Photo is valid if it meets all requirements // Photo is valid if it meets all requirements
const isValid: boolean = isPortraitValid const isValid: boolean = isPortraitValid
@ -105,6 +124,7 @@ export class AnalysisDialogContent extends
&& isGoryValid && isGoryValid
&& keywordsValid; && keywordsValid;
// Set the state so we can refresh the status
this.setState({ this.setState({
isAnalyzing: false, isAnalyzing: false,
analysis, analysis,
@ -118,7 +138,8 @@ export class AnalysisDialogContent extends
isRacyValid, isRacyValid,
isGoryValid, isGoryValid,
keywordsValid, keywordsValid,
invalidKeywords invalidKeywords,
celebrity: celebName
}); });
} }
@ -138,7 +159,8 @@ export class AnalysisDialogContent extends
isLinedrawingValid, isLinedrawingValid,
onlyOnePersonValid, onlyOnePersonValid,
invalidKeywords, invalidKeywords,
keywordsValid } = this.state; keywordsValid,
celebrity } = this.state;
if (analysis !== undefined) { if (analysis !== undefined) {
console.log("Analysis", analysis); console.log("Analysis", analysis);
@ -187,6 +209,11 @@ export class AnalysisDialogContent extends
<div className={styles.iconContainer} ><Icon iconName={isValid ? "CheckMark" : "StatusCircleErrorX"} className={css(styles.icon, isValid ? styles.iconGood : styles.iconBad)} /></div> <div className={styles.iconContainer} ><Icon iconName={isValid ? "CheckMark" : "StatusCircleErrorX"} className={css(styles.icon, isValid ? styles.iconGood : styles.iconBad)} /></div>
} }
{!isAnalyzing && celebrity !== undefined &&
<div><p>{Text.format(strings.YouLookLikeACelebrity, celebrity)}</p></div>
}
{!isAnalyzing && isValid && {!isAnalyzing && isValid &&
<div>{strings.AnalysisGoodLabel}</div> <div>{strings.AnalysisGoodLabel}</div>
} }
@ -215,12 +242,12 @@ export class AnalysisDialogContent extends
} }
{this.state.isSubmitted && {this.state.isSubmitted &&
<MessageBar <MessageBar
messageBarType={MessageBarType.success} messageBarType={MessageBarType.success}
isMultiline={false} isMultiline={false}
> >
{strings.SuccessMessage} {strings.SuccessMessage}
</MessageBar> </MessageBar>
} }
</Panel> </Panel>
@ -240,45 +267,48 @@ export class AnalysisDialogContent extends
} }
private onUpdateProfilePhoto = async (_ev?: React.SyntheticEvent<HTMLElement, Event>) => { private onUpdateProfilePhoto = async (_ev?: React.SyntheticEvent<HTMLElement, Event>) => {
console.log("Submitting photo"); // Get image array buffer
const profileBlob: Blob = this.props.blob; const profileBlob: Blob = this.props.blob;
// Get image array buffer // Submit using the approach you want
this.updateProfilePic(profileBlob); this.updateProfilePicUsingGraph(profileBlob);
//this.updateProfilePicUsingPnP(profileBlob);
} }
private async updateProfilePic(buffer) { // private async updateProfilePicUsingPnP(blob: Blob) {
console.log("Update profile pic", buffer); // pnpSetup({
// spfxContext: this.props.context
// });
this.props.context.msGraphClientFactory // console.log("Update profile pic using PnP", blob);
.getClient().then((client: MSGraphClient) => { // const response = await sp.profiles.setMyProfilePic(blob);
client // console.log("Profile property Updated", response);
.api("me/photo/$value") // this.setState({
.version("v1.0").header("Content-Type", buffer.type).put(buffer, (error, res) => { // isSubmitted: true
if (error) { // });
// }
// Update photo using Graph.
// See https://docs.microsoft.com/en-us/graph/api/profilephoto-update?view=graph-rest-1.0&tabs=http
private async updateProfilePicUsingGraph(blob: Blob) {
this.props.context.msGraphClientFactory
.getClient().then((client: MSGraphClient) => {
client
.api("me/photo/$value")
.version("v1.0").header("Content-Type", blob.type).put(blob, (error, _res) => {
if (error) {
console.log("Error updating profile", error); console.log("Error updating profile", error);
} else { } else {
console.log("Profile property Updated"); console.log("Profile property Updated");
this.setState({ this.setState({
isSubmitted: true isSubmitted: true
}); });
} }
}); });
}); });
} }
private onDismiss = (_ev?: React.SyntheticEvent<HTMLElement, Event>) => { private onDismiss = (_ev?: React.SyntheticEvent<HTMLElement, Event>) => {
this.props.onDismiss(); this.props.onDismiss();
} }
// private dataURLtoBlob = (dataurl: string): Blob => {
// var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
// bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
// while (n--) {
// u8arr[n] = bstr.charCodeAt(n);
// }
// return new Blob([u8arr], { type: mime });
// }
} }

View File

@ -16,4 +16,5 @@ export interface IAnalysisDialogContentState {
keywordsValid?: boolean; keywordsValid?: boolean;
invalidKeywords?: string[]; invalidKeywords?: string[];
isSubmitted: boolean; isSubmitted: boolean;
} celebrity?: string;
}

View File

@ -289,8 +289,6 @@ export default class ProfilePhotoEditor extends React.Component<IProfilePhotoEdi
// Get the image to approve // Get the image to approve
const imageToApprove: string = this.cropper.getCroppedCanvas().toDataURL(); const imageToApprove: string = this.cropper.getCroppedCanvas().toDataURL();
this.cropper.getCroppedCanvas().toBlob((blob: Blob)=> { this.cropper.getCroppedCanvas().toBlob((blob: Blob)=> {
console.log("Blob", blob);
const photoRequirements: IPhotoRequirements = { const photoRequirements: IPhotoRequirements = {
allowAdult: this.props.allowAdult, allowAdult: this.props.allowAdult,
allowClipart: this.props.allowClipart, allowClipart: this.props.allowClipart,

View File

@ -10,6 +10,9 @@ import styles from './WebCamDialog.module.scss';
import Webcam from "react-webcam"; import Webcam from "react-webcam";
/**
* Set the video constraints to be a square from the user-facing camera
*/
const videoConstraints = { const videoConstraints = {
width: 300, width: 300,
height: 300, height: 300,
@ -41,8 +44,6 @@ export class WebCamDialog extends React.Component<IWebCamDialogProps, IWebCamDia
screenshotFormat="image/jpeg" screenshotFormat="image/jpeg"
videoConstraints={videoConstraints} videoConstraints={videoConstraints}
imageSmoothing={true} imageSmoothing={true}
onUserMedia={() => console.log("OnUserMedia")}
onUserMediaError={() => console.log("OnUserMediaError")}
screenshotQuality={0.92} screenshotQuality={0.92}
/> />
@ -54,12 +55,19 @@ export class WebCamDialog extends React.Component<IWebCamDialogProps, IWebCamDia
); );
} }
/**
* Captures an image from the web cam
*/
private onCapture = () => { private onCapture = () => {
const imageSrc = this.webcamRef.getScreenshot(); const imageSrc = this.webcamRef.getScreenshot();
console.log("ImageSrc", imageSrc);
this.props.onCapture(imageSrc); this.props.onCapture(imageSrc);
} }
/**
*
* Dismisses the dialog
* @param _ev
*/
private onDismiss = (_ev?: React.SyntheticEvent<HTMLElement, Event>) => { private onDismiss = (_ev?: React.SyntheticEvent<HTMLElement, Event>) => {
this.props.onDismiss(); this.props.onDismiss();
} }

View File

@ -1,5 +1,6 @@
define([], function() { define([], function() {
return { return {
YouLookLikeACelebrity: "Uh... did anyone ever tell you that you look like {0}? It's uncanny!",
CaptureButtonLabel: "Capture", CaptureButtonLabel: "Capture",
WebCamDialogTitle: "Insert photo from camera", WebCamDialogTitle: "Insert photo from camera",
NoKeywords: "(none)", NoKeywords: "(none)",

View File

@ -1,4 +1,5 @@
declare interface IProfilePhotoEditorWebPartStrings { declare interface IProfilePhotoEditorWebPartStrings {
YouLookLikeACelebrity: string;
CaptureButtonLabel: string; CaptureButtonLabel: string;
WebCamDialogTitle: string; WebCamDialogTitle: string;
NoKeywords: string; NoKeywords: string;