springbootweb

This commit is contained in:
yinkanglong
2023-11-09 22:30:19 +08:00
parent 1d9b1b1fc5
commit f31bf5475d
2528 changed files with 1198944 additions and 10 deletions

View File

@@ -1,5 +1,43 @@
# web开发
- [web开发](#web开发)
- [关键](#关键)
- [1 静态资源访问](#1--静态资源访问)
- [静态资源访问](#静态资源访问)
- [欢迎页和图表](#欢迎页和图表)
- [自动加载原理](#自动加载原理)
- [2 请求映射处理](#2-请求映射处理)
- [请求映射过程](#请求映射过程)
- [请求映射原理](#请求映射原理)
- [请求处理的过程(类层侧结构)](#请求处理的过程类层侧结构)
- [3 请求参数处理](#3-请求参数处理)
- [注解请求参数](#注解请求参数)
- [传入ServletAPI](#传入servletapi)
- [复杂参数](#复杂参数)
- [自定义对象](#自定义对象)
- [自定义数据转换器](#自定义数据转换器)
- [4 响应数据处理](#4-响应数据处理)
- [响应数据](#响应数据)
- [自定义数据转换器](#自定义数据转换器-1)
- [自定义内容协商器](#自定义内容协商器)
- [5 视图解析与模板引擎](#5-视图解析与模板引擎)
- [服务端模板渲染](#服务端模板渲染)
- [视图解析原理](#视图解析原理)
## 关键
spring到处都是这种设计模式设置多个不同的处理器然后通过遍历循环找到支持当前类型的处理器如果存在效率问题则直接将当前条件对应的处理器缓存下来。这也是spring底层提供的大量扩展点。使用到的地方包括
* 映射处理器*MappingHandler找到不同的controller处理请求
* 参数解析器*ParamResolver
* 参数类型转换器*Convertor
* 返回值处理器*ResultProcessor
* 返回值消息转换器*MessageConvertor
* 视图解析器ViewResolver
* 错误异常处理HandlerExceptionResolvers:异常解析器,捕获并处理异常。
* 错误视图解析器DefaultErrorViewReslver:如果没有捕获处理异常则自动跳转到错误视图/error
> 特别疑惑这种通过循环遍历的方式查找对应的处理器的方法,是不是存在效率问题呢?
## 1 静态资源访问
### 静态资源访问
@@ -749,13 +787,7 @@ public static boolean isSimpleValueType(Class<?> type) {
}
```
> spring到处都是这种设计模式设置多个不同的处理器然后通过遍历循环找到支持当前类型的处理器如果存在效率问题则直接将当前条件对应的处理器缓存下来。使用到的地方包括
> * 参数解析器*ParamResolver
> * 返回值处理器*ResultProcessor
> * 类型转换器*Convertor
> * 映射处理器*MappingHandler
>
> 特别疑惑这种通过循环遍历的方式查找对应的处理器的方法,是不是存在效率问题呢?
### 自定义数据转换器
* 例如一下是自定义数据转换器的过程。
@@ -792,4 +824,203 @@ public static boolean isSimpleValueType(Class<?> type) {
}
};
}
```
```
## 4 响应数据处理
### 响应数据
1. 引入json开发的前端依赖就会自动将返回值使用jackson工具处理封装。
```java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
```
2. 返回值处理器判断是否支持该返回类型。
1. 上述返回值处理器支持的返回值类型。
![](image/2023-10-30-21-43-35.png)
![](image/2023-10-30-21-50-29.png)
```java
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
@ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor
```
4. ResponseBodyProcessor对返回值进行处理转换为JSON
1. 浏览器与服务器协商,服务器根据自身提供的类型能力,选择合适的类型。
2. SpringMVC查找所有的MessageConvertor找到能够处理数据的转换器。
![](image/2023-10-30-22-14-24.png)
![](image/2023-10-30-22-17-16.png)
```java
0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式xml处理的。
```
5. jackson2组件能够吧任何对象转换为json
6. 可以开启基于请求参数的内容协商。
1. spring.contentnegotiation.favor-param=true
2. ?format=json
### 自定义数据转换器
1. 需要自定义MessageConvertor。在SpringMVCConfigur中进行spring-mvc的自定义。使用mvc默认的扩展点
```java
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
}
}
```
2. 学习一种根据类自动加载MessageConvertor的方法。java的灵活之处也是java莫名其妙不知道在哪里给你添加一段代码的烦人的地方。
```java
WebMvcConfigurationSupport
//判断是否引入相关的依赖(与条件注解@OnConditionClass判断的原理一样,代码格式的判断)
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
//如果存在相关的依赖,则加载对应的类。
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
```
### 自定义内容协商器
1. 使用mvc默认的扩展点。
## 5 视图解析与模板引擎
### 服务端模板渲染
1. 相关的场景启动器
```java
spring-boot-starter-freemarker
spring-boot-starter-groovy-templates
spring-boot-starter-thymeleaf
```
2. thymeleaf自动配置类.
1. 自动配置好了thymeleaf的模板解析器SpringResourceTemplateResolver
2. 自动配置了spring的模板引擎。SpringTemplateEngine
3. 自动配置了Spring视图解析器ThymeleafViewResolverConfiguration
```java
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
```
3. thymeleaf 说明
1. 文本值: 'one text' , 'Another one!' ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false
2. 空值: null
3. 变量: onetwo.... 变量不能有空格
4. 字符串拼接: +
5. 变量替换: |The name is ${name}|
6. 运算符: + , - , * , / , %
7. 运算符: and , or一元运算: ! , not
8. 比较: > , < , >= , <= ( gt , lt , ge , le )等式: == , != ( eq , ne )
9. If-then: (if) ? (then) If-then-else: (if) ? (then) : (else) Default: (value) ?: (defaultvalue)
4. 表单写法
```java
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
```
5. 链接写法
```java
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
```
6. 循环写法
```java
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
```
7. 条件写法
```java
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>
```
8. 优先级
![](image/2023-11-02-21-26-18.png)
### 视图解析原理
带上 model、request、response调用render方法。
resolverName方法会遍历所有的视图解析器。找到支持的视图解析器。
1. 返回值“redirect:view”会得到RedirectView视图解析器。
2. 返回值“forward:”会在后端进行重定向,而不是前端浏览器。
3. 返回值 result方法。
通过视图解析器能够得到对应的视图对象。
调用视图对象的render方法进行渲染得到最终结果。
调用sendRedirect方法servlet的原生的redirect方法。使得前端

View File

@@ -0,0 +1,175 @@
## 拦截器
## 1 Servlet中的Filter
Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序主要的用途是设置字符集、控制权限、控制转向、做一些业务逻辑判断等。
### Filter有如下几个用处。
* 在HttpServletRequest到达Servlet之前拦截客户的HttpServletRequest。
* 根据需要检查HttpServletRequest也可以修改HttpServletRequest头和数据。
* 在HttpServletResponse到达客户端之前拦截HttpServletResponse。
* 根据需要检查HttpServletResponse也可以修改HttpServletResponse头和数据。
### Filter有如下几个种类。
* 用户授权的FilterFilter负责检查用户请求根据请求过滤用户非法请求。
* 日志Filter详细记录某些特殊的用户请求。
* 负责解码的Filter:包括对非标准编码的请求解码。
* 能改变XML内容的XSLT Filter等。
* Filter可以负责拦截多个请求或响应一个请求或响应也可以被多个Filter拦截。
### Filter使用
创建Filter必须实现javax.servlet.Filter接口在该接口中定义了如下三个方法。
* void init(FilterConfig config):用于完成Filter的初始化。
* void destory():用于Filter销毁前完成某些资源的回收。
* void doFilter(ServletRequest request,ServletResponse response,FilterChain chain):实现过滤功能,该方法就是对每个请求及响应增加的额外处理。该方法可以实现对用户请求进行预处理(ServletRequest request),也可实现对服务器响应进行后处理(ServletResponse response)—它们的分界线为是否调用了chain.doFilter(),执行该方法之前,即对用户请求进行预处理;执行该方法之后,即对服务器响应进行后处理
### Filter实例
```java
@WebFilter(filterName = "myFilter",urlPatterns = "/*")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
}
@Override
public void destroy() {
}
}
@SpringBootApplication
@EnableAutoConfiguration
@EnableWebMvc
@ServletComponentScan(basePackages = "com.my.test.filter")//所扫描的包路径必须包含该Filter
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application .class, args);
}
```
## 2 Spring中的Filter
### 说明
HandlerInterceptor 的功能跟过滤器类似但是提供更精细的的控制能力在request被响应之前、request被响应之后、视图渲染之前以及request全部结束之后。我们不能通过拦截器修改request内容但是可以通过抛出异常或者返回false来暂停request的执行。
### 使用步骤
1. 编写一个拦截器。继承实现WebRequestInterceptor的类、实现了Spring 的HandlerInterceptor 接口
```java
package org.example.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.example.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
log.info("preHandle intercept the uri:{}",uri);
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
if (null == user){
// request.setAttribute("msg","please login first");
//request.getRequestDispatcher("/login").forward(request,response);
request.getSession().setAttribute("msg","please login firse");
response.sendRedirect("/login");
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle intercept the uri:{}",request.getRequestURI());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion intercept thr uri:{}",request.getRequestURI());
}
}
```
1. 注册拦截器
```java
@Configuration
public class WebMvnConfig implements WebMvcConfigurer{
@Autowired
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/plugins/**","/dist/**","/login");
}
}
```
## 3 对比
### 对比
| Filter | HandlerInterceptor | 不同点 |
|---------------------------------------------------------------|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| Filter接口定义在Javax.servlet包中 | 接口HandlerInterceptor定义在org.springframework.web.servlet包中。 | 所在包不同 |
| Filter在Servlet前后起作用,Filters通常将请求和相应当做黑盒Filter通常不考虑servlet的实现 | 拦截器能够深入到方法前后、异常抛出前后等因此拦截器的使用具有更大的弹性。允许用户介入hook into请求的生命周期在请求过程中获取信息Interceptor 通常和请求更加耦合。 | 在Spring构架的程序中要优先使用拦截器。几乎所有 Filter 能够做的事情, interceptor 都能够轻松的实现 |
| Filter 是 Servlet 规范规定的。&nbsp;&nbsp; &nbsp; | 而拦截器既可以用于Web程序也可以用于Application、Swing程序中。&nbsp;&nbsp; &nbsp; | 使用范围不同 |
| Filter是Servlet规范中定义的是Servlet容器支持的&nbsp;&nbsp; &nbsp; | 拦截器在Spring容器内由Spring进行管理&nbsp;&nbsp; &nbsp; | 规范不同 |
| Filter不能够使用Spring容器资源&nbsp;&nbsp; &nbsp; | 拦截器是Spring的组件归Spring管理配置在Spring文件中因此可以使用Spring里的任何资源对象等&nbsp;&nbsp; &nbsp; | Spring中使用Interceptor更加容易 |
| Filter是被Server(tomcat etc)调用&nbsp;&nbsp; &nbsp; | Interceptor是被Spring调用&nbsp;&nbsp; &nbsp; | Filter总是优先于Intreceptor执行 |
### 顺序
Filter前处理 --> Interceptor前处理 --> controller--> Interceptor后处理 --> Filter后处理
## 4 拦截器的原理
1. 首先找到拦截器处理链。可以看到内部存储了多个拦截器。
![](image/2023-11-05-13-17-38.png)
2. 顺序执行所有拦截器的preHnadler方法
1. 如果返回为true则顺序执行下一个拦截器
2. 如果返回为false则逆序执行已经执行过的拦截器的afterhandler方法。则不会执行目标方法。
![](image/2023-11-05-13-19-33.png)
3. 如果目标方法执行结束,则倒序执行所有的拦截器。
4. 任何步骤出现异常都会触发afterCompletation
5. 页面成功渲染完成后也会倒序触发aftercompletion
![](image/2023-11-05-13-24-20.png)

View File

@@ -0,0 +1,61 @@
## 文件上传
1. 设置文件上传的表单
```java
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>
```
2. 在服务端接收文件上传的参数。
```java
/**
* MultipartFile 自动封装上传过来的文件
* @param email
* @param username
* @param headerImg
* @param photos
* @return
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息email={}username={}headerImg={}photos={}",
email,username,headerImg.getSize(),photos.length);
if(!headerImg.isEmpty()){
//保存到文件服务器OSS服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("H:\\cache\\"+originalFilename));
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("H:\\cache\\"+originalFilename));
}
}
}
return "main";
}
```
3. 设置允许文件上传的大小。
## 文件上传原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
● 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
● 原理步骤
○ 1、请求进来使用文件上传解析器判断isMultipart并封装resolveMultipart返回MultipartHttpServletRequest文件上传请求
○ 2、参数解析器来解析请求中的文件内容封装成MultipartFile
○ 3、将request中文件信息封装为一个MapMultiValueMap<String, MultipartFile>
FileCopyUtils。实现文件流的拷贝
![](image/2023-11-05-13-50-21.png)

View File

@@ -0,0 +1,113 @@
## 1 机制
### 1、默认规则
* 默认情况下Spring Boot提供/error处理所有错误的映射
* 对于机器客户端它将生成JSON响应其中包含错误HTTP状态和异常消息的详细信息。对于浏览器客户端响应一个“ whitelabel”错误视图以HTML格式呈现相同的数据
![](image/2023-11-05-14-19-08.png)
![](image/2023-11-05-14-19-19.png)
* 要对其进行自定义添加View解析为error
* 要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
* error/下的4xx5xx页面会被自动解析
![](image/2023-11-05-14-20-04.png)
### 2、定制错误处理逻辑
* 自定义错误页
* error/404.html error/5xx.html有精确的错误状态码页面就匹配精确没有就找 4xx.html如果都没有就触发白页。抛出异常给Tomcat捕获异常forward到error视图进行处理由BasicErrorController处理springboot底层自动注册的error视图
* @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
* @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver 把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason)tomcat发送的/error
* Spring底层的异常如 参数类型转换异常DefaultHandlerExceptionResolver 处理框架底层的异常。
○ response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
![](image/2023-11-05-14-21-08.png)
● 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
![](image/2023-11-05-14-21-18.png)
* ErrorViewResolver 实现自定义处理异常;
* response.sendError 。error请求就会转给controller
* 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
* basicErrorController 要去的页面地址是 ErrorViewResolver
### 3、异常处理自动配置原理
ErrorMvcAutoConfiguration 自动配置异常处理规则
* 容器中的组件类型DefaultErrorAttributes -> iderrorAttributes
* public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
* DefaultErrorAttributes定义错误页面中可以包含哪些数据。
![](image/2023-11-05-14-22-08.png)
![](image/2023-11-05-14-22-14.png)
* 容器中的组件类型BasicErrorController --> idbasicErrorControllerjson+白页 适配响应)
* 处理默认 /error 路径的请求;页面响应 new ModelAndView("error", model)
* 容器中有组件 View->id是error响应默认错误页
* 容器中放组件 BeanNameViewResolver视图解析器按照返回的视图名作为组件的id去容器中找View对象。
* 容器中的组件类型DefaultErrorViewResolver -> idconventionErrorViewResolver
* 如果发生错误会以HTTP的状态码 作为视图页地址viewName找到真正的页面
* error/404、5xx.html
* 如果想要返回页面就会找error视图【StaticView】。(默认是一个白页)
* 写出去json ![](image/2023-11-05-14-23-22.png)
* 错误页HTML ![](image/2023-11-05-14-23-30.png)
### 4、异常处理步骤流程
1. 执行目标方法目标方法运行期间有任何异常都会被catch、而且标志当前请求结束并且用 dispatchException
2. 进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
3. mv = processHandlerException处理handler发生的异常处理完成返回ModelAndView
4. 遍历所有的 handlerExceptionResolvers看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】![](image/2023-11-05-14-24-56.png)
5. 系统默认的 异常解析器;![](image/2023-11-05-14-25-03.png)
6. DefaultErrorAttributes先来处理异常。把异常信息保存到request域并且返回null
7. 默认没有任何人能处理异常,所以异常会被抛出
8. **如果没有任何人能处理最终底层就会发送 /error 请求**。会被底层的BasicErrorController处理
9. 解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。![](image/2023-11-05-14-25-11.png)
10. 默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址error/500.html
11. 模板引擎最终响应这个页面 error/500.html
## 5、自定义异常处理
### @ControllerAdvice + @ExceptionHandler
1. 通过@ControllerAdvice和@ExceptionHandler处理异常
2. 有ExceptionHandlerExceptionResolver在启动的时候扫描注解加载异常处理方法。
```java
@ControllerAdvice
public class ExceptionHandler{
@ExceptionHandler({MessageNotFoundException.class})
public String getMessage(Exception exception){
return "error";
}
}
```
### ExceptionHandlerResolver
1. 可以作为全局处理器
2.
```java
@Orderd(value = Ordered.Highest)
@Component
public class CustomerHandlerExceptionResolver implement HanlderExcepitonResolver{
@Override
public ModoleAndView resovlerException(){
//异常处理逻辑
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB