Pridani editace fotek
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
"""Add photo adjustment
|
||||||
|
|
||||||
|
Revision ID: 2acb6fa61028
|
||||||
|
Revises: 956d295689bf
|
||||||
|
Create Date: 2023-10-15 22:33:04.911765
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2acb6fa61028'
|
||||||
|
down_revision = '956d295689bf'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('photo_adjustment',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('photo_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('rotate', sa.Float(), nullable=True),
|
||||||
|
sa.Column('contrast', sa.Float(), nullable=True),
|
||||||
|
sa.Column('brightness', sa.Float(), nullable=True),
|
||||||
|
sa.Column('saturation', sa.Float(), nullable=True),
|
||||||
|
sa.Column('sharpness', sa.Float(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['photo_id'], ['photo.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('photo_adjustment')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""add crop info
|
||||||
|
|
||||||
|
Revision ID: c3aef4ae2669
|
||||||
|
Revises: 2acb6fa61028
|
||||||
|
Create Date: 2023-10-20 23:53:21.772545
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c3aef4ae2669'
|
||||||
|
down_revision = '2acb6fa61028'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('photo_adjustment', sa.Column('crop_left', sa.Float(), nullable=True))
|
||||||
|
op.add_column('photo_adjustment', sa.Column('crop_top', sa.Float(), nullable=True))
|
||||||
|
op.add_column('photo_adjustment', sa.Column('crop_width', sa.Float(), nullable=True))
|
||||||
|
op.add_column('photo_adjustment', sa.Column('crop_height', sa.Float(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('photo_adjustment', 'crop_height')
|
||||||
|
op.drop_column('photo_adjustment', 'crop_width')
|
||||||
|
op.drop_column('photo_adjustment', 'crop_top')
|
||||||
|
op.drop_column('photo_adjustment', 'crop_left')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""add size to photos
|
||||||
|
|
||||||
|
Revision ID: 4996ad8bc231
|
||||||
|
Revises: c3aef4ae2669
|
||||||
|
Create Date: 2023-10-24 16:32:10.826763
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4996ad8bc231'
|
||||||
|
down_revision = 'c3aef4ae2669'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('photo', sa.Column('width', sa.Integer(), nullable=False))
|
||||||
|
op.add_column('photo', sa.Column('height', sa.Integer(), nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('photo', 'height')
|
||||||
|
op.drop_column('photo', 'width')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -19,14 +19,13 @@ async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
|
|||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
await models.Flight.update(
|
await models.Flight.update(
|
||||||
db, {"gpx_track_filename": output_name, "has_terrain_elevation": True},
|
db, {"gpx_track_filename": output_name, "has_terrain_elevation": True},
|
||||||
id=flight_id)
|
id=flight_id
|
||||||
|
)
|
||||||
except ClientResponseError as e:
|
except ClientResponseError as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
async def add_terrain_elevation_to_photo(photo):
|
async def add_terrain_elevation_to_photo(photo):
|
||||||
async with get_session() as db:
|
|
||||||
try:
|
try:
|
||||||
elevation = await elevation_api.get_elevation_for_points([
|
elevation = await elevation_api.get_elevation_for_points([
|
||||||
{"lat": photo.gps_latitude, "lng": photo.gps_longitude}
|
{"lat": photo.gps_latitude, "lng": photo.gps_longitude}
|
||||||
@@ -36,6 +35,7 @@ async def add_terrain_elevation_to_photo(photo):
|
|||||||
return
|
return
|
||||||
|
|
||||||
terrain_elevation = elevation[0]['elevation']
|
terrain_elevation = elevation[0]['elevation']
|
||||||
|
async with get_session() as db:
|
||||||
await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation})
|
await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Cannot get elevation: {e}")
|
print(f"Cannot get elevation: {e}")
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
from utils.image import resize_image
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
from utils.image import PhotoEditor
|
||||||
|
|
||||||
|
|
||||||
async def resize_photo(path: str, filename: str):
|
async def resize_photo(path: str, filename: str, photo_id: int):
|
||||||
return await resize_image(path, filename, new_width=2500, quality=85)
|
editor = PhotoEditor(path, filename)
|
||||||
|
editor.resize(new_width=2500)
|
||||||
|
editor.write_to_file(quality=85)
|
||||||
|
width, height = editor.img_size
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
await models.Photo.update(db, {"width": width, "height": height}, id=photo_id)
|
||||||
|
|
||||||
|
|
||||||
async def generate_thumbnail(path: str, filename: str, quality: int = 90):
|
async def generate_thumbnail(path: str, filename: str):
|
||||||
return await resize_image(
|
editor = PhotoEditor(path, filename)
|
||||||
path, filename,
|
editor.resize(new_width=300)
|
||||||
new_width=300,
|
editor.write_to_file(quality=85, dest_path=f"{path}/thumbs/")
|
||||||
dest_path=f"{path}/thumbs/",
|
|
||||||
dest_filename=filename,
|
|
||||||
quality=quality
|
|
||||||
)
|
|
||||||
@@ -118,6 +118,8 @@ class Photo(BaseModel):
|
|||||||
filename: Mapped[str] = mapped_column(String(128), nullable=False)
|
filename: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0")
|
is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0")
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
width: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
height: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
@@ -130,9 +132,31 @@ class Photo(BaseModel):
|
|||||||
|
|
||||||
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
|
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
|
||||||
point_of_interest: Mapped['PointOfInterest'] = relationship()
|
point_of_interest: Mapped['PointOfInterest'] = relationship()
|
||||||
|
adjustment: Mapped['PhotoAdjustment'] = relationship()
|
||||||
created_by: Mapped['User'] = relationship()
|
created_by: Mapped['User'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoAdjustment(BaseModel):
|
||||||
|
__tablename__ = "photo_adjustment"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=False)
|
||||||
|
rotate: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
contrast: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
brightness: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
saturation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
sharpness: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
crop_left: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_top: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_width: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_height: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
photo: Mapped['Photo'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
class Aircraft(BaseModel):
|
class Aircraft(BaseModel):
|
||||||
__tablename__ = "aircraft"
|
__tablename__ = "aircraft"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import os.path
|
||||||
|
from sqlalchemy import select
|
||||||
|
from starlette.responses import StreamingResponse
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
from endpoints.base import AuthEndpoint
|
||||||
|
from paths import get_photo_basepath
|
||||||
|
from utils.image import PhotoEditor
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoEditorEndpoint(AuthEndpoint):
|
||||||
|
async def show_preview(self, photo_id: int, **kwargs):
|
||||||
|
async with get_session() as db:
|
||||||
|
photo = (await db.scalars(
|
||||||
|
select(models.Photo)
|
||||||
|
.filter(models.Photo.id == photo_id)
|
||||||
|
)).one()
|
||||||
|
|
||||||
|
basepath = get_photo_basepath(photo.flight_id)
|
||||||
|
filename = photo.filename
|
||||||
|
|
||||||
|
original_filename = '_original_'+filename
|
||||||
|
if os.path.exists(f"{basepath}/{original_filename}"):
|
||||||
|
filename = original_filename
|
||||||
|
|
||||||
|
editor = PhotoEditor(basepath, filename)
|
||||||
|
editor.resize(new_height=900)
|
||||||
|
# TODO: idealni je udelat co nejdriv resize
|
||||||
|
# velikost muze ovlivnit: orez, otoceni, coz jsou dve nejnarocnejsi operace...
|
||||||
|
# ^^ tohle by bylo fajn cachovat
|
||||||
|
|
||||||
|
if kwargs.get("rotate"):
|
||||||
|
editor.rotate(kwargs['rotate'], True)
|
||||||
|
|
||||||
|
crop = {
|
||||||
|
key.replace("crop_", ""): value
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
if key in ('crop_left', 'crop_top', 'crop_width', 'crop_height') and value is not None
|
||||||
|
}
|
||||||
|
if crop and len(crop.keys()) == 4:
|
||||||
|
editor.crop(**crop)
|
||||||
|
|
||||||
|
adjustments = {
|
||||||
|
key: value for key, value in kwargs.items()
|
||||||
|
if key in ('saturation', 'brightness', 'contrast', 'sharpness') and value is not None
|
||||||
|
}
|
||||||
|
if adjustments:
|
||||||
|
editor.adjust(**adjustments)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return StreamingResponse(content=editor.get_as_stream(), media_type="image/jpeg")
|
||||||
|
|
||||||
@@ -27,11 +27,13 @@ flights_by_copilot_dataloader = DataLoader(
|
|||||||
extra_join=[models.Flight.copilots]).load,
|
extra_join=[models.Flight.copilots]).load,
|
||||||
cache=False
|
cache=False
|
||||||
)
|
)
|
||||||
|
|
||||||
flights_by_aircraft_dataloader = DataLoader(
|
flights_by_aircraft_dataloader = DataLoader(
|
||||||
load_fn=MultiModelsDataloader(
|
load_fn=MultiModelsDataloader(
|
||||||
models.Flight,
|
models.Flight,
|
||||||
relationship_column=models.Flight.aircraft_id
|
relationship_column=models.Flight.aircraft_id
|
||||||
).load, cache=False)
|
).load, cache=False)
|
||||||
|
|
||||||
flight_by_poi_dataloader = DataLoader(
|
flight_by_poi_dataloader = DataLoader(
|
||||||
load_fn=MultiModelsDataloader(
|
load_fn=MultiModelsDataloader(
|
||||||
models.Flight,
|
models.Flight,
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ organizations_dataloader = create_dataloader(models.Organization)
|
|||||||
airport_weather_info_loader = create_dataloader(models.WeatherInfo)
|
airport_weather_info_loader = create_dataloader(models.WeatherInfo)
|
||||||
poi_dataloader = create_dataloader(models.PointOfInterest)
|
poi_dataloader = create_dataloader(models.PointOfInterest)
|
||||||
poi_type_dataloader = create_dataloader(models.PointOfInterestType)
|
poi_type_dataloader = create_dataloader(models.PointOfInterestType)
|
||||||
|
flight_dataloader = create_dataloader(models.Flight)
|
||||||
|
photo_adjustment_dataloader = create_dataloader(models.PhotoAdjustment, relationship_column=models.PhotoAdjustment.photo_id)
|
||||||
|
photo_dataloader = create_dataloader(models.Photo)
|
||||||
cover_photo_loader = create_dataloader(
|
cover_photo_loader = create_dataloader(
|
||||||
models.Photo,
|
models.Photo,
|
||||||
relationship_column=models.Photo.flight_id,
|
relationship_column=models.Photo.flight_id,
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ from typing import List, Optional
|
|||||||
import strawberry
|
import strawberry
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
from decorators.endpoints import authenticated_user_only
|
||||||
from database.transaction import get_session
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
||||||
from graphql_schema.entities.resolvers.event import EventQueryResolver
|
from graphql_schema.entities.resolvers.event import EventQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
|
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import List
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import List, Optional
|
||||||
import strawberry
|
import strawberry
|
||||||
|
from PIL import Image
|
||||||
|
from sqlalchemy import delete, select
|
||||||
|
|
||||||
from background_jobs.elevation import add_terrain_elevation_to_photo
|
from background_jobs.elevation import add_terrain_elevation_to_photo
|
||||||
from background_jobs.photo import generate_thumbnail, resize_photo
|
from background_jobs.photo import generate_thumbnail, resize_photo
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
from decorators.endpoints import authenticated_user_only
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
||||||
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver
|
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver
|
||||||
from graphql_schema.entities.types.types import Photo
|
from graphql_schema.entities.types.types import Photo, PhotoAdjustment
|
||||||
|
from logger import log
|
||||||
from paths import get_photo_basepath
|
from paths import get_photo_basepath
|
||||||
from utils.file import delete_file
|
from utils.file import delete_file
|
||||||
from utils.image import parse_exif_info, rotate_image
|
from utils.image import parse_exif_info, rotate_image_no_crop, adjust_image, resize_image, PhotoEditor
|
||||||
from utils.upload import handle_file_upload
|
from utils.upload import handle_file_upload
|
||||||
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput
|
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
@@ -22,6 +28,10 @@ class PhotoQueries:
|
|||||||
async def photos(root, info) -> List[Photo]:
|
async def photos(root, info) -> List[Photo]:
|
||||||
return await BaseQueryResolver(Photo, models.Photo).get_list(user_id=info.context.user_id)
|
return await BaseQueryResolver(Photo, models.Photo).get_list(user_id=info.context.user_id)
|
||||||
|
|
||||||
|
@strawberry.field()
|
||||||
|
async def photo(root, info, id: int) -> Photo:
|
||||||
|
return await BaseQueryResolver(Photo, models.Photo).get_one(id, user_id=info.context.user_id)
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PhotoMutation:
|
class PhotoMutation:
|
||||||
@@ -32,12 +42,16 @@ class PhotoMutation:
|
|||||||
filename = await handle_file_upload(input.photo, path)
|
filename = await handle_file_upload(input.photo, path)
|
||||||
exif_info = await parse_exif_info(path, filename)
|
exif_info = await parse_exif_info(path, filename)
|
||||||
|
|
||||||
|
img = Image.open(f"{path}/{filename}")
|
||||||
|
|
||||||
photo = await PhotoMutationResolver().create(
|
photo = await PhotoMutationResolver().create(
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
data={
|
data={
|
||||||
"flight_id": input.flight_id,
|
"flight_id": input.flight_id,
|
||||||
"name": input.name,
|
"name": input.name,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
|
"width": img.width,
|
||||||
|
"height": img.height,
|
||||||
"description": input.description,
|
"description": input.description,
|
||||||
"exposed_at": exif_info.get("datetime_original"),
|
"exposed_at": exif_info.get("datetime_original"),
|
||||||
"gps_latitude": exif_info.get("gps_latitude"),
|
"gps_latitude": exif_info.get("gps_latitude"),
|
||||||
@@ -47,7 +61,7 @@ class PhotoMutation:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
info.context.background_tasks.add_task(resize_photo, path=path, filename=filename)
|
info.context.background_tasks.add_task(resize_photo, path=path, filename=filename, photo_id=photo.id)
|
||||||
info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename)
|
info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename)
|
||||||
|
|
||||||
if exif_info.get("gps_latitude") and exif_info.get("gps_longitude"):
|
if exif_info.get("gps_latitude") and exif_info.get("gps_longitude"):
|
||||||
@@ -62,27 +76,99 @@ class PhotoMutation:
|
|||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation()
|
||||||
@authenticated_user_only()
|
@authenticated_user_only()
|
||||||
async def rotate_photo(self, info, id: int, angle: int) -> Photo:
|
async def change_orientation(self, info, id: int, direction: str) -> Photo:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
photo = (await db.scalars(
|
photo = (await db.scalars(
|
||||||
BaseQueryResolver(Photo, models.Photo).get_query(user_id=info.context.user_id, object_id=id)
|
BaseQueryResolver(Photo, models.Photo).get_query(user_id=info.context.user_id, object_id=id)
|
||||||
)).one()
|
)).one()
|
||||||
photo_filename = photo.filename
|
|
||||||
flight_id = photo.flight_id
|
flight_id = photo.flight_id
|
||||||
|
photo_id = photo.id
|
||||||
|
photo_filename = photo.filename
|
||||||
|
|
||||||
|
photo_path = get_photo_basepath(flight_id)
|
||||||
|
original_filename = "_original_" + photo_filename
|
||||||
|
if not os.path.isfile(photo_path + "/" + original_filename):
|
||||||
|
shutil.copyfile(photo_path + "/" + photo_filename, photo_path + "/" + original_filename)
|
||||||
|
|
||||||
|
degrees_map = {
|
||||||
|
"clockwise": -90,
|
||||||
|
"counterClockwise": 90
|
||||||
|
}
|
||||||
|
|
||||||
|
# rotate original
|
||||||
|
editor = PhotoEditor(photo_path, original_filename)
|
||||||
|
editor.rotate(degrees=degrees_map[direction], crop_after_rotate=False)
|
||||||
|
editor.write_to_file(quality=100)
|
||||||
|
|
||||||
|
# rotate possibly adjusted image
|
||||||
|
editor = PhotoEditor(photo_path, photo_filename)
|
||||||
|
editor.rotate(degrees=degrees_map[direction], crop_after_rotate=False)
|
||||||
|
editor.write_to_file(quality=100)
|
||||||
|
|
||||||
|
info.context.background_tasks.add_task(generate_thumbnail, path=photo_path, filename=photo_filename)
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
photo = await models.Photo.update(db, {
|
||||||
|
"width": editor.img.width,
|
||||||
|
"height": editor.img.height
|
||||||
|
}, id=photo_id)
|
||||||
|
return Photo(**photo.as_dict())
|
||||||
|
|
||||||
|
@strawberry.mutation()
|
||||||
|
@authenticated_user_only()
|
||||||
|
async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo:
|
||||||
|
async with get_session() as db:
|
||||||
|
photo = (await db.scalars(
|
||||||
|
BaseQueryResolver(Photo, models.Photo).get_query(user_id=info.context.user_id, object_id=id)
|
||||||
|
)).one()
|
||||||
|
flight_id = photo.flight_id
|
||||||
|
photo_filename = photo.filename
|
||||||
photo_as_dict = photo.as_dict()
|
photo_as_dict = photo.as_dict()
|
||||||
|
|
||||||
await asyncio.gather(
|
photo_path = get_photo_basepath(flight_id)
|
||||||
rotate_image(
|
|
||||||
path=get_photo_basepath(flight_id),
|
# TOOD: presunout do samostatne metody
|
||||||
filename=photo_filename,
|
original_filename = "_original_" + photo_filename
|
||||||
angle=angle,
|
if not os.path.isfile(photo_path + "/" + original_filename):
|
||||||
),
|
shutil.copyfile(photo_path + "/" + photo_filename, photo_path + "/" + original_filename)
|
||||||
rotate_image(
|
|
||||||
path=get_photo_basepath(flight_id) + "/thumbs",
|
editor = (
|
||||||
filename=photo_filename,
|
PhotoEditor(photo_path, original_filename)
|
||||||
angle=angle,
|
.adjust(
|
||||||
),
|
brightness=adjustment.brightness,
|
||||||
|
contrast=adjustment.contrast,
|
||||||
|
sharpness=adjustment.sharpness,
|
||||||
|
saturation=adjustment.saturation
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if adjustment.rotate:
|
||||||
|
rotate_angle = adjustment.rotate
|
||||||
|
editor.rotate(rotate_angle, adjustment.crop_after_rotate)
|
||||||
|
|
||||||
|
if adjustment.crop:
|
||||||
|
editor.crop(**adjustment.crop.to_dict())
|
||||||
|
|
||||||
|
editor.write_to_file(dest_filename=photo_filename)
|
||||||
|
|
||||||
|
info.context.background_tasks.add_task(generate_thumbnail, path=photo_path, filename=photo_filename)
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
await db.execute(delete(models.PhotoAdjustment).filter(models.PhotoAdjustment.photo_id == id))
|
||||||
|
|
||||||
|
crop_info = {"crop_" + key: value for key, value in adjustment.crop.to_dict().items()} if adjustment.crop else {}
|
||||||
|
await models.PhotoAdjustment.create(db, {
|
||||||
|
"photo_id": id,
|
||||||
|
"contrast": adjustment.contrast,
|
||||||
|
"saturation": adjustment.saturation,
|
||||||
|
"brightness": adjustment.brightness,
|
||||||
|
"rotate": adjustment.rotate,
|
||||||
|
"sharpness": adjustment.sharpness,
|
||||||
|
**crop_info
|
||||||
|
})
|
||||||
|
|
||||||
|
width, height = editor.img_size
|
||||||
|
await models.Photo.update(db, {"width": width, "height": height}, id=id)
|
||||||
|
|
||||||
return Photo(**photo_as_dict)
|
return Photo(**photo_as_dict)
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,25 @@ class EditPhotoInput:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.input
|
||||||
|
class CropInput(BaseGraphqlInputType):
|
||||||
|
left: float
|
||||||
|
top: float
|
||||||
|
width: float
|
||||||
|
height: float
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.input
|
||||||
|
class AdjustmentInput:
|
||||||
|
rotate: Optional[float] = 0
|
||||||
|
crop_after_rotate: Optional[bool] = True,
|
||||||
|
brightness: Optional[float] = 1
|
||||||
|
contrast: Optional[float] = 1
|
||||||
|
saturation: Optional[float] = 1
|
||||||
|
sharpness: Optional[float] = 1
|
||||||
|
crop: Optional[CropInput] = None
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
|
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
|
||||||
"id", "aircraft_id", "deleted", "landing_airport_id", "takeoff_airport_id",
|
"id", "aircraft_id", "deleted", "landing_airport_id", "takeoff_airport_id",
|
||||||
"takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename", "event_id"
|
"takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename", "event_id"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from graphql_schema.dataloaders.multi_models import (
|
|||||||
)
|
)
|
||||||
from graphql_schema.dataloaders.single_model import (
|
from graphql_schema.dataloaders.single_model import (
|
||||||
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, cover_photo_loader,
|
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, cover_photo_loader,
|
||||||
airport_weather_info_loader, organizations_dataloader
|
airport_weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader, photo_dataloader
|
||||||
)
|
)
|
||||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
|
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
|
||||||
from paths import (
|
from paths import (
|
||||||
@@ -65,6 +65,11 @@ class WeatherInfo:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_sqlalchemy_type(models.PhotoAdjustment)
|
||||||
|
class PhotoAdjustment:
|
||||||
|
photo: Photo = strawberry.field(resolver=lambda root: photo_dataloader.load(root.photo_id))
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.PointOfInterest)
|
@strawberry_sqlalchemy_type(models.PointOfInterest)
|
||||||
class PointOfInterest:
|
class PointOfInterest:
|
||||||
type: Optional[PointOfInterestType] = strawberry.field(resolver=lambda root: poi_type_dataloader.load(root.type_id))
|
type: Optional[PointOfInterestType] = strawberry.field(resolver=lambda root: poi_type_dataloader.load(root.type_id))
|
||||||
@@ -79,6 +84,8 @@ class Photo:
|
|||||||
point_of_interest: Optional[PointOfInterest] = strawberry.field(
|
point_of_interest: Optional[PointOfInterest] = strawberry.field(
|
||||||
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
|
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
|
||||||
)
|
)
|
||||||
|
flight: Flight = strawberry.field(resolver=lambda root: flight_dataloader.load(root.flight_id))
|
||||||
|
adjustment: Optional[PhotoAdjustment] = strawberry.field(resolver=lambda root: photo_adjustment_dataloader.load(root.id))
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.Flight)
|
@strawberry_sqlalchemy_type(models.Flight)
|
||||||
|
|||||||
+33
@@ -1,4 +1,6 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, APIRouter, Depends, Security
|
from fastapi import FastAPI, APIRouter, Depends, Security
|
||||||
from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie
|
from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -10,6 +12,7 @@ from strawberry.fastapi import GraphQLRouter
|
|||||||
from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG, ALLOW_CORS_ORIGINS
|
from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG, ALLOW_CORS_ORIGINS
|
||||||
from database import models, async_session
|
from database import models, async_session
|
||||||
from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint
|
from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint
|
||||||
|
from endpoints.photo_editor_preview import PhotoEditorEndpoint
|
||||||
from endpoints.registration import RegistrationInput, RegistrationEndpoint
|
from endpoints.registration import RegistrationInput, RegistrationEndpoint
|
||||||
from graphql_schema.schema import schema, GraphQLContext
|
from graphql_schema.schema import schema, GraphQLContext
|
||||||
|
|
||||||
@@ -121,6 +124,36 @@ class App:
|
|||||||
refresh_token=self.refresh_security
|
refresh_token=self.refresh_security
|
||||||
).on_post(resp)
|
).on_post(resp)
|
||||||
|
|
||||||
|
@self.api_router.get("/photo/editor-preview/{photo_id}")
|
||||||
|
async def photo_editor_preview(
|
||||||
|
photo_id: int,
|
||||||
|
brightness: Optional[float] = None,
|
||||||
|
contrast: Optional[float] = None,
|
||||||
|
saturation: Optional[float] = None,
|
||||||
|
sharpness: Optional[float] = None,
|
||||||
|
rotate: Optional[float] = None,
|
||||||
|
crop_left: Optional[float] = None,
|
||||||
|
crop_top: Optional[float] = None,
|
||||||
|
crop_width: Optional[float] = None,
|
||||||
|
crop_height: Optional[float] = None,
|
||||||
|
):
|
||||||
|
return await PhotoEditorEndpoint(
|
||||||
|
access_token=self.access_security,
|
||||||
|
refresh_token=self.refresh_security
|
||||||
|
).show_preview(
|
||||||
|
photo_id=photo_id,
|
||||||
|
logged_user_id=0,
|
||||||
|
saturation=saturation,
|
||||||
|
brightness=brightness,
|
||||||
|
contrast=contrast,
|
||||||
|
sharpness=sharpness,
|
||||||
|
crop_top=crop_top,
|
||||||
|
crop_left=crop_left,
|
||||||
|
crop_height=crop_height,
|
||||||
|
crop_width=crop_width,
|
||||||
|
rotate=rotate,
|
||||||
|
)
|
||||||
|
|
||||||
@self.api_router.post("/registration", status_code=201)
|
@self.api_router.post("/registration", status_code=201)
|
||||||
async def registration(user: RegistrationInput):
|
async def registration(user: RegistrationInput):
|
||||||
return await RegistrationEndpoint().on_post(user)
|
return await RegistrationEndpoint().on_post(user)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from PIL import Image
|
||||||
|
from PIL.Image import DecompressionBombWarning
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
sys.path.insert(0, "/app/src")
|
||||||
|
from paths import get_photo_basepath # noqa
|
||||||
|
from database import async_session, models # noqa
|
||||||
|
|
||||||
|
|
||||||
|
async def add_sizes_to_photos():
|
||||||
|
async with async_session() as session:
|
||||||
|
photos = (await session.scalars(
|
||||||
|
select(models.Photo)
|
||||||
|
)).all()
|
||||||
|
|
||||||
|
for photo in photos:
|
||||||
|
path = get_photo_basepath(photo.flight_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
img = Image.open(f"{path}/{photo.filename}")
|
||||||
|
await models.Photo.update(session, {"width": img.width, "height": img.height}, obj=photo)
|
||||||
|
print(path, photo.filename, img.width, img.height, "OK")
|
||||||
|
except (Exception, DecompressionBombWarning) as e:
|
||||||
|
print(path, photo.filename, e)
|
||||||
|
|
||||||
|
await session.flush()
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(add_sizes_to_photos())
|
||||||
@@ -2,10 +2,8 @@ import asyncio
|
|||||||
import sys
|
import sys
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from paths import FLIGHT_GPX_TRACK_PATH
|
|
||||||
|
|
||||||
sys.path.insert(0, "/app/src")
|
sys.path.insert(0, "/app/src")
|
||||||
|
from paths import FLIGHT_GPX_TRACK_PATH # noqa
|
||||||
from database import async_session, models # noqa
|
from database import async_session, models # noqa
|
||||||
from external.elevation import elevation_api # noqa
|
from external.elevation import elevation_api # noqa
|
||||||
from external.gpx_parser import GPXParser # noqa
|
from external.gpx_parser import GPXParser # noqa
|
||||||
|
|||||||
+189
-4
@@ -1,7 +1,11 @@
|
|||||||
|
import io
|
||||||
|
|
||||||
|
import math
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
import exif
|
import exif
|
||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
|
from PIL.ImageEnhance import Brightness, Contrast, Color, Sharpness
|
||||||
from utils.file import check_directories
|
from utils.file import check_directories
|
||||||
from utils.gps import gps_to_decimal
|
from utils.gps import gps_to_decimal
|
||||||
|
|
||||||
@@ -27,6 +31,114 @@ async def parse_exif_info(path: str, filename: str) -> dict:
|
|||||||
return exif_info
|
return exif_info
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoEditor:
|
||||||
|
def __init__(self, path: str, filename: str):
|
||||||
|
self.path = path
|
||||||
|
self.filename = filename
|
||||||
|
|
||||||
|
self.img = Image.open(f"{path}/{filename}")
|
||||||
|
self.img_size = self.img.size
|
||||||
|
|
||||||
|
def resize(self, new_width: Optional[int] = None, new_height: Optional[int] = None):
|
||||||
|
if not new_width and not new_height:
|
||||||
|
raise ValueError("Set either new_width or new_height")
|
||||||
|
|
||||||
|
w, h = self.img.size
|
||||||
|
aspect_ratio = w / h
|
||||||
|
|
||||||
|
if not new_width:
|
||||||
|
# TODO: tohle bude asi blbe, tu se bude muset asi delit?
|
||||||
|
new_width = int(new_height * aspect_ratio)
|
||||||
|
|
||||||
|
if not new_height:
|
||||||
|
new_height = int(new_width / aspect_ratio)
|
||||||
|
|
||||||
|
self.img = self.img.resize((new_width, new_height), Image.BICUBIC)
|
||||||
|
self.img_size = self.img.size
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _get_cropbox_after_rotate(self, degrees: float, rotated_width: int, rotated_height: int) -> Tuple[int, int]:
|
||||||
|
original_width, original_height = self.img_size
|
||||||
|
aspect_ratio = float(original_width) / original_height
|
||||||
|
rotated_aspect_ratio = float(rotated_width) / rotated_height
|
||||||
|
angle = math.fabs(degrees) * math.pi / 180
|
||||||
|
|
||||||
|
if aspect_ratio < 1:
|
||||||
|
total_height = float(original_width) / rotated_aspect_ratio
|
||||||
|
else:
|
||||||
|
total_height = float(original_height)
|
||||||
|
|
||||||
|
h = total_height / (aspect_ratio * math.sin(angle) + math.cos(angle))
|
||||||
|
w = h * aspect_ratio
|
||||||
|
|
||||||
|
return round(w), round(h)
|
||||||
|
|
||||||
|
def rotate(self, degrees: float, crop_after_rotate: bool = False):
|
||||||
|
self.img = self.img.rotate(degrees, resample=Image.Resampling.BICUBIC, expand=True)
|
||||||
|
|
||||||
|
if crop_after_rotate:
|
||||||
|
rotated_width, rotated_height = self.img.size
|
||||||
|
new_width, new_height = self._get_cropbox_after_rotate(degrees, rotated_width, rotated_height)
|
||||||
|
|
||||||
|
left = round((rotated_width - new_width) / 2)
|
||||||
|
top = round((rotated_height - new_height) / 2)
|
||||||
|
|
||||||
|
self.img = self.img.crop((left, top, new_width, new_height))
|
||||||
|
|
||||||
|
self.img_size = self.img.size
|
||||||
|
return self
|
||||||
|
|
||||||
|
def crop(self, left: float, top: float, width: float, height: float):
|
||||||
|
w, h = self.img_size
|
||||||
|
left_px = int(left * w)
|
||||||
|
top_px = int(top * h)
|
||||||
|
width_px = int(width * w)
|
||||||
|
height_px = int(height * h)
|
||||||
|
|
||||||
|
self.img = self.img.crop((left_px, top_px, left_px + width_px, top_px + height_px))
|
||||||
|
self.img_size = self.img.size
|
||||||
|
return self
|
||||||
|
|
||||||
|
def adjust(
|
||||||
|
self,
|
||||||
|
brightness: Optional[float] = None,
|
||||||
|
contrast: Optional[float] = None,
|
||||||
|
saturation: Optional[float] = None,
|
||||||
|
sharpness: Optional[float] = None
|
||||||
|
):
|
||||||
|
adjustments = [
|
||||||
|
(Brightness, brightness),
|
||||||
|
(Contrast, contrast),
|
||||||
|
(Color, saturation),
|
||||||
|
(Sharpness, sharpness)
|
||||||
|
]
|
||||||
|
for adjustment, value in adjustments:
|
||||||
|
if value is not None:
|
||||||
|
self.img = adjustment(self.img).enhance(value)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_as_stream(self):
|
||||||
|
img_io = io.BytesIO()
|
||||||
|
self.img.save(img_io, 'JPEG')
|
||||||
|
img_io.seek(0)
|
||||||
|
|
||||||
|
return img_io
|
||||||
|
|
||||||
|
def write_to_file(
|
||||||
|
self, quality: int = 90, dest_path: Optional[str] = None, dest_filename: Optional[str] = None
|
||||||
|
) -> str:
|
||||||
|
dest = f"{dest_path or self.path}/{dest_filename or self.filename}"
|
||||||
|
self.img.save(dest, 'JPEG', quality=quality)
|
||||||
|
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -----------
|
||||||
|
# odsud niz to bude vse asi na smazani, vse by mela umet trida PhotoEditor
|
||||||
|
|
||||||
async def resize_image(
|
async def resize_image(
|
||||||
path: str, filename: str, new_width: int, quality: int = 90,
|
path: str, filename: str, new_width: int, quality: int = 90,
|
||||||
dest_path: Optional[str] = None,
|
dest_path: Optional[str] = None,
|
||||||
@@ -47,14 +159,34 @@ async def resize_image(
|
|||||||
image = image.resize((new_width, new_height), Image.LANCZOS)
|
image = image.resize((new_width, new_height), Image.LANCZOS)
|
||||||
check_directories(dest_path)
|
check_directories(dest_path)
|
||||||
image.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=quality)
|
image.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=quality)
|
||||||
|
|
||||||
|
return new_width, new_height
|
||||||
except UnidentifiedImageError:
|
except UnidentifiedImageError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def rotate_image(
|
def crop_image_after_rotate(
|
||||||
|
original_width: int, original_height: int, rotated_width: int, rotated_height: int, degrees: float
|
||||||
|
) -> Tuple[int, int]:
|
||||||
|
aspect_ratio = float(original_width) / original_height
|
||||||
|
rotated_aspect_ratio = float(rotated_width) / rotated_height
|
||||||
|
angle = math.fabs(degrees) * math.pi / 180
|
||||||
|
|
||||||
|
if aspect_ratio < 1:
|
||||||
|
total_height = float(original_width) / rotated_aspect_ratio
|
||||||
|
else:
|
||||||
|
total_height = float(original_height)
|
||||||
|
|
||||||
|
h = total_height / (aspect_ratio * math.sin(angle) + math.cos(angle))
|
||||||
|
w = h * aspect_ratio
|
||||||
|
|
||||||
|
return round(w), round(h)
|
||||||
|
|
||||||
|
|
||||||
|
async def rotate_image_no_crop(
|
||||||
path: str,
|
path: str,
|
||||||
filename: str,
|
filename: str,
|
||||||
angle: int,
|
angle: float,
|
||||||
dest_path: Optional[str] = None,
|
dest_path: Optional[str] = None,
|
||||||
dest_filename: Optional[str] = None
|
dest_filename: Optional[str] = None
|
||||||
):
|
):
|
||||||
@@ -65,6 +197,59 @@ async def rotate_image(
|
|||||||
dest_filename = filename
|
dest_filename = filename
|
||||||
|
|
||||||
img = Image.open(f"{path}/{filename}")
|
img = Image.open(f"{path}/{filename}")
|
||||||
img = img.rotate(angle, Image.LANCZOS, expand=True)
|
rotated_img = img.rotate(angle, Image.BICUBIC, expand=True)
|
||||||
|
|
||||||
check_directories(dest_path)
|
check_directories(dest_path)
|
||||||
|
rotated_img.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=100)
|
||||||
|
|
||||||
|
|
||||||
|
async def adjust_image(
|
||||||
|
path: str,
|
||||||
|
filename: str,
|
||||||
|
rotate: float,
|
||||||
|
brightness: float,
|
||||||
|
contrast: float,
|
||||||
|
saturation: float,
|
||||||
|
sharpness: float,
|
||||||
|
crop: dict,
|
||||||
|
dest_path: Optional[str] = None,
|
||||||
|
dest_filename: Optional[str] = None
|
||||||
|
):
|
||||||
|
if not dest_path:
|
||||||
|
dest_path = path
|
||||||
|
|
||||||
|
if not dest_filename:
|
||||||
|
dest_filename = filename
|
||||||
|
|
||||||
|
img = Image.open(f"{path}/{filename}")
|
||||||
|
|
||||||
|
if rotate:
|
||||||
|
rotated_img = img.rotate(rotate, Image.BICUBIC, expand=True)
|
||||||
|
|
||||||
|
width, height = img.size
|
||||||
|
rotated_width, rotated_height = rotated_img.size
|
||||||
|
new_width, new_height = crop_image_after_rotate(width, height, rotated_width, rotated_height, rotate)
|
||||||
|
|
||||||
|
left = round((rotated_width - new_width) / 2)
|
||||||
|
top = round((rotated_height - new_height) / 2)
|
||||||
|
|
||||||
|
img = rotated_img.crop((left, top, left + new_width, top + new_height))
|
||||||
|
|
||||||
|
adjustments = [
|
||||||
|
(Brightness, brightness),
|
||||||
|
(Contrast, contrast),
|
||||||
|
(Color, saturation),
|
||||||
|
(Sharpness, sharpness)
|
||||||
|
]
|
||||||
|
for adj, value in adjustments:
|
||||||
|
img = adj(img).enhance(value)
|
||||||
|
|
||||||
|
if crop:
|
||||||
|
w, h = img.size
|
||||||
|
left = crop['left'] * w
|
||||||
|
top = crop['top'] * h
|
||||||
|
width = crop['width'] * w
|
||||||
|
height = crop['height'] * h
|
||||||
|
img = img.crop((left, top, left + width, top + height))
|
||||||
|
|
||||||
img.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=100)
|
img.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=100)
|
||||||
|
|||||||
Reference in New Issue
Block a user