当前位置:   article > 正文

uniapp使用Canvas给图片加水印把临时文件上传到服务器

uniapp使用Canvas给图片加水印把临时文件上传到服务器

生成的临时路径是没有完整的路径没办法上传到服务器

  1. 16:37:40.993 添加水印后的路径, _doc/uniapp_temp_1710923708347/canvas/17109238597881.png
  2. 16:37:41.041 添加水印后的完整路径, file://storage/emulated/0/Android/data/com.jingruan.zjd/apps/__UNI__BE4B000/doc/uniapp_temp_1710923708347/canvas/17109238597881.png

使用以下代码得到完整的路径

let path = 'file:/' + plus.io.convertLocalFileSystemURL(tempFilePath);


 

完整代码如下 使用的插件市场的hpy-watermark组件   一共2个

效果是

其他页面调用方式

  1. <!--
  2. 增加水印上传villageReviewForm.preciseAddr 是通过高德获取的定位地址
  3. -->
  4. <hpy-watermark ref="uploadImage" :address="villageReviewForm.preciseAddr" @waterMark="waterMark"></hpy-watermark>

高德获取定位

  1. uni.getLocation({
  2. type: 'gcj02',
  3. geocode: true,
  4. isHighAccuracy: true,
  5. success: res => {
  6. const {
  7. province,
  8. city,
  9. district,
  10. street,
  11. streetNum,
  12. poiName
  13. } = res.address;
  14. this.villageReviewForm.preciseAddr =
  15. `${district}${street}${streetNum}${poiName}${res.longitude},${res.latitude}`;
  16. console.log("经纬度地点",this.villageReviewForm.preciseAddr)
  17. // 数据渲染
  18. this.$forceUpdate();
  19. }
  20. });

获取上传数据结果

const fileList = this.$refs.uploadImage.fileList

组件样式

hpy-watermark.vue

  1. <template>
  2. <view>
  3. <view class="watermark-content">
  4. <canvas canvas-id="watermarkCanvas" id="watermarkCanvas" :style="{width:canvasWidth + 'px', height:canvasHeight + 'px'}"></canvas>
  5. </view>
  6. <upload-image v-model="fileList" style="margin-left: 15rpx" :image-styles="imageStyles" :files-list="filesList" :delIcon="delIcon" @choose="chooseImage" @delFile="delFile">
  7. <slot>
  8. <view class="is-add">
  9. <view class="icon-add"></view>
  10. <view class="icon-add rotate"></view>
  11. </view>
  12. </slot>
  13. </upload-image>
  14. </view>
  15. </template>
  16. <script>
  17. import {
  18. fileServerIp
  19. } from "@/common/utils/config.js"
  20. import Session from "@/common/Session";
  21. import uploadImage from './upload-image.vue'
  22. export default {
  23. components: {
  24. uploadImage,
  25. },
  26. name:'hpy-watermark',
  27. props:{
  28. address:{
  29. type:String,
  30. default:''
  31. },
  32. delIcon: {
  33. type: Boolean,
  34. default: true
  35. },
  36. listStyles: {
  37. type: Object,
  38. default () {
  39. return {
  40. // 是否显示边框
  41. border: true,
  42. // 是否显示分隔线
  43. dividline: true,
  44. // 线条样式
  45. borderStyle: {}
  46. }
  47. }
  48. },
  49. imageStyles: {
  50. type: Object,
  51. default () {
  52. return {
  53. width: 'auto',
  54. height: 'auto'
  55. }
  56. }
  57. },
  58. /**
  59. * 文字文字位置(默认:左下角)可选值:左上角:topLeft、右上角:topRight、左下角:bottomLeft、右下角:bottomRight
  60. */
  61. markAlign:{
  62. type:String,
  63. default:function(){
  64. return 'bottomLeft'
  65. }
  66. },
  67. /**
  68. * 设置文本的水平对齐方式,默认:start,文本在指定的位置开始。
  69. * end 文本在指定的位置结束。
  70. * center 文本的中心被放置在指定的位置。
  71. * left 文本左对齐。
  72. * right 文本右对齐。
  73. */
  74. textAlign:{
  75. type:String,
  76. default:function(){
  77. return 'start';
  78. }
  79. },
  80. /**
  81. * 设置文本的垂直对齐方式,默认:alphabetic文本基线是普通的字母基线。
  82. * top 文本基线是 em 方框的顶端。
  83. * hanging 文本基线是悬挂基线。
  84. * middle 文本基线是 em 方框的正中。
  85. * ideographic 文本基线是表意基线。
  86. * bottom 文本基线是 em 方框的底端。
  87. */
  88. textBaseline:{
  89. type:String,
  90. default:function(){
  91. return 'alphabetic';
  92. }
  93. },
  94. /**
  95. * 文字大小
  96. */
  97. fontSize:{
  98. type:[Number, String],
  99. default:30
  100. },
  101. /**
  102. * 文字颜色
  103. */
  104. fontColor:{
  105. type:String,
  106. default:function(){
  107. return 'red'
  108. }
  109. },
  110. /**
  111. * 阴影颜色
  112. */
  113. shadowColor:{
  114. type:String,
  115. default:function(){
  116. return 'rgba(0, 0, 0, 1.0)';
  117. }
  118. },
  119. /**
  120. * 阴影边框大小
  121. */
  122. shadowWidth:{
  123. type:[Number, String],
  124. default:2
  125. },
  126. /**
  127. * 图片的质量,取值范围为 (0, 1],不在范围内时当作1处理
  128. */
  129. quality:{
  130. type:[Number, String],
  131. default:1
  132. },
  133. /**
  134. * 目标文件的类型,只支持 'jpg' 或 'png'。默认为 'png'
  135. */
  136. fileType:{
  137. type:String,
  138. default:function(){
  139. return 'png'
  140. }
  141. }
  142. },
  143. data() {
  144. return {
  145. fileList: [],
  146. files: [],
  147. filesList:[],
  148. canvasWidth:0,
  149. canvasHeight:0
  150. };
  151. },
  152. watch: {
  153. fileList: {
  154. handler(newVal, oldVal) {
  155. this.filesList=newVal;
  156. },
  157. immediate: true
  158. },
  159. },
  160. methods: {
  161. // 选择图片
  162. chooseImage() {
  163. if(this.isEmpty(this.address)){
  164. uni.showToast({
  165. icon:'none',
  166. title:'请打开定位或者重新获取'
  167. });
  168. return;
  169. }
  170. uni.chooseImage({
  171. count: this.limit, // 限制的图片数量
  172. sizeType: ['compressed'], // original 原图,compressed 压缩图,默认二者都有
  173. sourceType: [ 'camera'],// album 从相册选图,camera 使用相机,默认二者都有
  174. success: (res) => {
  175. var imgPathList = res.tempFilePaths;
  176. if(imgPathList.length > 0){
  177. this.addImages(imgPathList);
  178. }
  179. },
  180. fail: (err) => {
  181. console.log('chooseImage fail', err)
  182. if("chooseImage:fail cancel" == err.errMsg){
  183. uni.showToast({
  184. icon:'none',
  185. title:'取消了选择'
  186. });
  187. }else{
  188. }
  189. }
  190. });
  191. },
  192. // 添加图片
  193. addImages(filePaths){
  194. if(filePaths.length > 0){
  195. var fillTexts = ["地址:"+this.address];
  196. fillTexts.push("时间:" + this.getNowTime());
  197. // 添加水印
  198. this.addWaterMark({
  199. filePaths,
  200. fillTexts
  201. });
  202. }
  203. },
  204. /**
  205. * 水印添加回调,在H5平台下,filePath 为 base64
  206. */
  207. waterMark(filePath){
  208. this.imageList.push(filePath);
  209. },
  210. /**
  211. * 获取当前时间
  212. */
  213. getNowTime(){
  214. var date = new Date(),
  215. year = date.getFullYear(),
  216. month = date.getMonth() + 1,
  217. day = date.getDate(),
  218. hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(),
  219. minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(),
  220. second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
  221. month >= 1 && month <= 9 ? (month = "0" + month) : "";
  222. day >= 0 && day <= 9 ? (day = "0" + day) : "";
  223. return (year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
  224. },
  225. /**
  226. * 删除文件
  227. * @param {Object} index
  228. */
  229. delFile(index) {
  230. this.$emit('delete', {
  231. tempFile: this.filesList[index],
  232. tempFilePath: this.filesList[index].url
  233. })
  234. this.filesList.splice(index, 1)
  235. },
  236. /**
  237. * 增加水印
  238. * @param {Object} {filePaths:['图片地址1', '图片地址2'], fillTexts:['水印1', '水印2']}
  239. */
  240. async addWaterMark({ filePaths = [], fillTexts = [] }) {
  241. try{
  242. for (const filePath of filePaths) {
  243. await this.drawImage(filePath, fillTexts.reverse());
  244. }
  245. }catch(e){
  246. // TODO handle the exception
  247. }finally{
  248. // uni.hideLoading();
  249. }
  250. },
  251. /**
  252. * 绘制单个图片
  253. */
  254. async drawImage(filePath, fillTexts){
  255. uni.showLoading({title:'图片处理中···'});
  256. const ctx = uni.createCanvasContext('watermarkCanvas', this);
  257. return new Promise(resolve => {
  258. uni.getImageInfo({
  259. src: filePath,
  260. success: (image) => {
  261. this.canvasWidth = image.width;
  262. this.canvasHeight = image.height;
  263. ctx.clearRect(0, 0, image.width, image.height);
  264. setTimeout(()=>{
  265. ctx.drawImage(image.path, 0, 0, image.width, image.height);
  266. ctx.setFontSize(this.fontSize);
  267. ctx.setFillStyle(this.fontColor);
  268. // 设置阴影
  269. let shadowWidth = Number(this.shadowWidth + "");
  270. if(shadowWidth > 0){
  271. ctx.shadowColor = this.shadowColor;
  272. ctx.shadowOffsetX = shadowWidth;
  273. ctx.shadowOffsetY = shadowWidth;
  274. }
  275. // 设置水平对齐方式
  276. ctx.textAlign = this.textAlign;
  277. // 设置垂直对齐方式
  278. ctx.textBaseline = this.textBaseline;
  279. const maxText = fillTexts.reduce((text, val) => {
  280. return text.length >= val.length ? text : val;
  281. });
  282. fillTexts.forEach((mark, index) => {
  283. if(this.markAlign == "bottomRight"){
  284. ctx.fillText(mark, image.width - (ctx.measureText(maxText).width+60), image.height - (index*60+60));
  285. }else if(this.markAlign == "topLeft"){
  286. ctx.fillText(mark, 20, (index*60+60));
  287. }else if(this.markAlign == "topRight"){
  288. ctx.fillText(mark, image.width - (ctx.measureText(maxText).width+60), (index*60+60));
  289. }else{
  290. ctx.fillText(mark, 20, image.height - (index*60+60));
  291. }
  292. });
  293. ctx.draw(false, (() => {
  294. setTimeout(()=>{
  295. uni.canvasToTempFilePath({
  296. canvasId: 'watermarkCanvas',
  297. fileType:this.fileType,
  298. quality:Number(this.quality + "" || "1"),
  299. success: (res) => {
  300. console.log("添加水印后的路径",res.tempFilePath )
  301. this.saveUploadImage(res.tempFilePath )
  302. },
  303. fail:(err) => {
  304. uni.hideLoading();
  305. console.log(err)
  306. },
  307. complete: () => {
  308. resolve();
  309. }
  310. }, this);
  311. }, 300);
  312. })());
  313. }, 200);
  314. },
  315. fail: (e) => {
  316. resolve();
  317. }
  318. });
  319. });
  320. },
  321. saveUploadImage(tempFilePath){
  322. uni.showLoading({title:'图片上传中···'});
  323. // #ifdef APP-PLUS
  324. var p = plus.io.convertLocalFileSystemURL(tempFilePath);
  325. this.url = 'file:/' + p
  326. console.log("添加水印后的完整路径",this.url )
  327. // #endif
  328. uni.uploadFile({
  329. url: fileServerIp + 'common/upload',
  330. name: "file",
  331. // #ifdef H5
  332. filePath: tempFilePath,
  333. // #endif
  334. // #ifdef APP-PLUS
  335. filePath: this.url,
  336. // #endif
  337. header: {
  338. Authorization: "Bearer " + Session.getValue('token')
  339. },
  340. success: uploadFileRes => {
  341. uni.hideLoading();
  342. const {
  343. data
  344. } = JSON.parse(uploadFileRes.data)
  345. this.filesList.push({
  346. url: data.url,
  347. name: data.fileName,
  348. extname: 'png'
  349. })
  350. this.$emit('waterMark',{
  351. url: data.url,
  352. name: data.fileName,
  353. extname: 'png'
  354. });
  355. },
  356. fail: error => {
  357. uni.hideLoading();
  358. uni.showToast({
  359. title: '上传失败!',
  360. icon: 'error',
  361. duration: 2000
  362. });
  363. }
  364. })
  365. }
  366. }
  367. }
  368. </script>
  369. <style scoped>
  370. .watermark-content{width: 0;height: 0;overflow: hidden;}
  371. .uni-file-picker__container {
  372. /* #ifndef APP-NVUE */
  373. display: flex;
  374. box-sizing: border-box;
  375. /* #endif */
  376. flex-wrap: wrap;
  377. margin: -5px;
  378. }
  379. .rotate {
  380. position: absolute;
  381. transform: rotate(90deg);
  382. }
  383. .icon-add {
  384. width: 50px;
  385. height: 5px;
  386. background-color: #f1f1f1;
  387. border-radius: 2px;
  388. }
  389. </style>

upload-image.vue

  1. <template>
  2. <view class="uni-file-picker__container">
  3. <view class="file-picker__box" v-for="(item,index) in filesList" :key="index" :style="boxStyle">
  4. <view class="file-picker__box-content" :style="borderStyle">
  5. <image class="file-image" :src="item.url" mode="aspectFill" @click.stop="prviewImage(item,index)"></image>
  6. <view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)">
  7. <view class="icon-del"></view>
  8. <view class="icon-del rotate"></view>
  9. </view>
  10. <view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress">
  11. <progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4"
  12. :backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" />
  13. </view>
  14. <view v-if="item.errMsg" class="file-picker__mask" @click.stop="uploadFiles(item,index)">
  15. 点击重试
  16. </view>
  17. </view>
  18. </view>
  19. <view v-if="filesList.length < limit && !readonly" class="file-picker__box" :style="boxStyle">
  20. <view class="file-picker__box-content is-add" :style="borderStyle" @click="choose">
  21. <slot>
  22. <view class="icon-add"></view>
  23. <view class="icon-add rotate"></view>
  24. </slot>
  25. </view>
  26. </view>
  27. </view>
  28. </template>
  29. <script>
  30. export default {
  31. name: "uploadImage",
  32. emits:['uploadFiles','choose','delFile'],
  33. props: {
  34. filesList: {
  35. type: Array,
  36. default () {
  37. return []
  38. }
  39. },
  40. disabled:{
  41. type: Boolean,
  42. default: false
  43. },
  44. disablePreview: {
  45. type: Boolean,
  46. default: false
  47. },
  48. limit: {
  49. type: [Number, String],
  50. default: 9
  51. },
  52. imageStyles: {
  53. type: Object,
  54. default () {
  55. return {
  56. width: 'auto',
  57. height: 'auto',
  58. border: {}
  59. }
  60. }
  61. },
  62. delIcon: {
  63. type: Boolean,
  64. default: true
  65. },
  66. readonly:{
  67. type:Boolean,
  68. default:false
  69. }
  70. },
  71. computed: {
  72. styles() {
  73. let styles = {
  74. width: 'auto',
  75. height: 'auto',
  76. border: {}
  77. }
  78. return Object.assign(styles, this.imageStyles)
  79. },
  80. boxStyle() {
  81. const {
  82. width = 'auto',
  83. height = 'auto'
  84. } = this.styles
  85. let obj = {}
  86. if (height === 'auto') {
  87. if (width !== 'auto') {
  88. obj.height = this.value2px(width)
  89. obj['padding-top'] = 0
  90. } else {
  91. obj.height = 0
  92. }
  93. } else {
  94. obj.height = this.value2px(height)
  95. obj['padding-top'] = 0
  96. }
  97. if (width === 'auto') {
  98. if (height !== 'auto') {
  99. obj.width = this.value2px(height)
  100. } else {
  101. obj.width = '33.3%'
  102. }
  103. } else {
  104. obj.width = this.value2px(width)
  105. }
  106. let classles = ''
  107. for(let i in obj){
  108. classles+= `${i}:${obj[i]};`
  109. }
  110. return classles
  111. },
  112. borderStyle() {
  113. let {
  114. border
  115. } = this.styles
  116. let obj = {}
  117. const widthDefaultValue = 1
  118. const radiusDefaultValue = 3
  119. if (typeof border === 'boolean') {
  120. obj.border = border ? '1px #eee solid' : 'none'
  121. } else {
  122. let width = (border && border.width) || widthDefaultValue
  123. width = this.value2px(width)
  124. let radius = (border && border.radius) || radiusDefaultValue
  125. radius = this.value2px(radius)
  126. obj = {
  127. 'border-width': width,
  128. 'border-style': (border && border.style) || 'solid',
  129. 'border-color': (border && border.color) || '#eee',
  130. 'border-radius': radius
  131. }
  132. }
  133. let classles = ''
  134. for(let i in obj){
  135. classles+= `${i}:${obj[i]};`
  136. }
  137. return classles
  138. }
  139. },
  140. methods: {
  141. uploadFiles(item, index) {
  142. this.$emit("uploadFiles", item)
  143. },
  144. choose() {
  145. this.$emit("choose")
  146. },
  147. delFile(index) {
  148. this.$emit('delFile', index)
  149. },
  150. prviewImage(img, index) {
  151. let urls = []
  152. if(Number(this.limit) === 1&&this.disablePreview&&!this.disabled){
  153. this.$emit("choose")
  154. }
  155. if(this.disablePreview) return
  156. this.filesList.forEach(i => {
  157. urls.push(i.url)
  158. })
  159. uni.previewImage({
  160. urls: urls,
  161. current: index
  162. });
  163. },
  164. value2px(value) {
  165. if (typeof value === 'number') {
  166. value += 'px'
  167. } else {
  168. if (value.indexOf('%') === -1) {
  169. value = value.indexOf('px') !== -1 ? value : value + 'px'
  170. }
  171. }
  172. return value
  173. }
  174. }
  175. }
  176. </script>
  177. <style lang="scss">
  178. .uni-file-picker__container {
  179. /* #ifndef APP-NVUE */
  180. display: flex;
  181. box-sizing: border-box;
  182. /* #endif */
  183. flex-wrap: wrap;
  184. margin: -5px;
  185. }
  186. .file-picker__box {
  187. position: relative;
  188. // flex: 0 0 33.3%;
  189. width: 33.3%;
  190. height: 0;
  191. padding-top: 33.33%;
  192. /* #ifndef APP-NVUE */
  193. box-sizing: border-box;
  194. /* #endif */
  195. }
  196. .file-picker__box-content {
  197. position: absolute;
  198. top: 0;
  199. right: 0;
  200. bottom: 0;
  201. left: 0;
  202. margin: 5px;
  203. border: 1px #eee solid;
  204. border-radius: 5px;
  205. overflow: hidden;
  206. }
  207. .file-picker__progress {
  208. position: absolute;
  209. bottom: 0;
  210. left: 0;
  211. right: 0;
  212. /* border: 1px red solid; */
  213. z-index: 2;
  214. }
  215. .file-picker__progress-item {
  216. width: 100%;
  217. }
  218. .file-picker__mask {
  219. /* #ifndef APP-NVUE */
  220. display: flex;
  221. /* #endif */
  222. justify-content: center;
  223. align-items: center;
  224. position: absolute;
  225. right: 0;
  226. top: 0;
  227. bottom: 0;
  228. left: 0;
  229. color: #fff;
  230. font-size: 12px;
  231. background-color: rgba(0, 0, 0, 0.4);
  232. }
  233. .file-image {
  234. width: 100%;
  235. height: 100%;
  236. }
  237. .is-add {
  238. /* #ifndef APP-NVUE */
  239. display: flex;
  240. /* #endif */
  241. align-items: center;
  242. justify-content: center;
  243. }
  244. .icon-add {
  245. width: 50px;
  246. height: 5px;
  247. background-color: #f1f1f1;
  248. border-radius: 2px;
  249. }
  250. .rotate {
  251. position: absolute;
  252. transform: rotate(90deg);
  253. }
  254. .icon-del-box {
  255. /* #ifndef APP-NVUE */
  256. display: flex;
  257. /* #endif */
  258. align-items: center;
  259. justify-content: center;
  260. position: absolute;
  261. top: 3px;
  262. right: 3px;
  263. height: 26px;
  264. width: 26px;
  265. border-radius: 50%;
  266. background-color: rgba(0, 0, 0, 0.5);
  267. z-index: 2;
  268. transform: rotate(-45deg);
  269. }
  270. .icon-del {
  271. width: 15px;
  272. height: 2px;
  273. background-color: #fff;
  274. border-radius: 2px;
  275. }
  276. </style>

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/280343
推荐阅读
相关标签
  

闽ICP备14008679号