@@ -4,6 +4,8 @@ import User from "../User/User.js"
44import Layer from "../Layer/Layer.js"
55import dbDriver from "../../database/driver.js"
66import vault from "../../utilities/vault.js"
7+ import imageSize from 'image-size' ;
8+ import mime from 'mime-types' ;
79
810const database = new dbDriver ( "mongo" )
911
@@ -150,6 +152,139 @@ export default class ProjectFactory {
150152 } )
151153 }
152154
155+ /**
156+ * Creates a new manifest from given image url and project label.
157+ * @param {string } imageUrl - URL of the image to be used in the project.
158+ * @param {string } label - Label for the project.
159+ * @returns {Object } - Returns the created project object.
160+ */
161+
162+ static async getImageDimensions ( imgUrl ) {
163+ try {
164+ const response = await fetch ( imgUrl )
165+ if ( ! response . ok ) {
166+ throw {
167+ status : response . status ,
168+ message : `Failed to fetch image: ${ response . statusText } `
169+ }
170+ }
171+ const arrayBuffer = await response . arrayBuffer ( )
172+ const buffer = Buffer . from ( arrayBuffer )
173+ const dimensions = imageSize ( buffer )
174+ return {
175+ width : dimensions . width ,
176+ height : dimensions . height
177+ }
178+ } catch ( err ) {
179+ console . error ( "Error fetching image dimensions:" , err . message )
180+ return
181+ }
182+ }
183+
184+ static async DBObjectFromImage ( manifest ) {
185+ if ( ! manifest ) {
186+ throw {
187+ status : 404 ,
188+ message : err . message ?? "No manifest found. Cannot process empty object"
189+ }
190+ }
191+ const _id = manifest . id . split ( '/' ) . slice ( - 2 , - 1 ) [ 0 ]
192+ const now = Date . now ( ) . toString ( ) . slice ( - 6 )
193+ const label = ProjectFactory . getLabelAsString ( manifest . label )
194+ const metadata = manifest . metadata ?? [ ]
195+ const layer = Layer . build ( _id , `First Layer - ${ label } ` , manifest . items )
196+
197+ const firstPage = layer . pages [ 0 ] ?. id . split ( '/' ) . pop ( ) ?? true
198+
199+ return {
200+ _id,
201+ label,
202+ metadata,
203+ manifest : [ manifest . id ] ,
204+ layers : [ layer . asProjectLayer ( ) ] ,
205+ tools : this . tools ,
206+ _createdAt : now ,
207+ _modifiedAt : - 1 ,
208+ _lastModified : firstPage ,
209+ }
210+ }
211+
212+ static async createManifestFromImage ( imageURL , projectLabel , creator ) {
213+ if ( ! imageURL ) {
214+ throw {
215+ status : 404 ,
216+ message : "No image found. Cannot process further."
217+ }
218+ }
219+ const _id = database . reserveId ( )
220+ const now = Date . now ( ) . toString ( ) . slice ( - 6 )
221+ const label = projectLabel ?? now
222+ const dimensions = await this . getImageDimensions ( imageURL )
223+
224+ const canvasLayout = {
225+ id : `${ process . env . TPENSTATIC } /${ _id } /canvas-1.json` ,
226+ type : "Canvas" ,
227+ label : { "none" : [ `${ label } Page 1` ] } ,
228+ width : dimensions . width ,
229+ height : dimensions . height ,
230+ items : [
231+ {
232+ id : `${ process . env . TPENSTATIC } /${ _id } /contentPage.json` ,
233+ type : "AnnotationPage" ,
234+ items : [
235+ {
236+ id : `${ process . env . TPENSTATIC } /${ _id } /content.json` ,
237+ type : "Annotation" ,
238+ motivation : "painting" ,
239+ body : {
240+ id : imageURL ,
241+ type : "Image" ,
242+ format : mime . lookup ( imageURL ) || "image/jpeg" ,
243+ width : dimensions . width ,
244+ height : dimensions . height
245+ } ,
246+ target : `${ process . env . TPENSTATIC } /${ _id } /canvas-1.json`
247+ }
248+ ]
249+ }
250+ ]
251+ }
252+
253+ const projectManifest = {
254+ "@context" : "http://iiif.io/api/presentation/3/context.json" ,
255+ id : `${ process . env . TPENSTATIC } /${ _id } /manifest.json` ,
256+ type : "Manifest" ,
257+ label : { "none" : [ label ] } ,
258+ items : [ canvasLayout ]
259+ }
260+
261+ const projectCanvas = {
262+ "@context" : "http://iiif.io/api/presentation/3/context.json" ,
263+ ...canvasLayout
264+ }
265+
266+ await this . uploadFileToGitHub ( projectManifest , _id )
267+ await this . uploadFileToGitHub ( projectCanvas , _id )
268+
269+ return await ProjectFactory . DBObjectFromImage ( projectManifest )
270+ . then ( async ( project ) => {
271+ const projectObj = new Project ( )
272+ const group = await Group . createNewGroup ( creator ,
273+ {
274+ label : project . label ?? project . title ?? `Project ${ new Date ( ) . toLocaleDateString ( ) } ` ,
275+ members : { [ creator ] : { roles : [ ] } }
276+ } )
277+ . then ( ( group ) => group . _id )
278+ return await projectObj . create ( { ...project , creator, group } )
279+ } )
280+ . catch ( ( err ) => {
281+ throw {
282+ status : err . status ?? 500 ,
283+ message : err . message ?? "Internal Server Error"
284+ }
285+ } )
286+ }
287+
153288 /**
154289 * Convert the Project.data into an Object ready for consumption by a TPEN interface,
155290 * especially the GET /project/:id endpoint.
@@ -321,7 +456,8 @@ export default class ProjectFactory {
321456 * - Uploads the file using the GitHub API, including the correct commit message and SHA for updates.
322457 */
323458 static async uploadFileToGitHub ( manifest , projectId ) {
324- const manifestUrl = `https://api.github.com/repos/${ process . env . REPO_OWNER } /${ process . env . REPO_NAME } /contents/${ projectId } /manifest.json`
459+ const fileName = manifest ?. id ?. split ( '/' ) . pop ( ) ?? 'manifest.json'
460+ const manifestUrl = `https://api.github.com/repos/${ process . env . REPO_OWNER } /${ process . env . REPO_NAME } /contents/${ projectId } /${ fileName } `
325461 const token = process . env . GITHUB_TOKEN
326462
327463 try {
@@ -339,23 +475,30 @@ export default class ProjectFactory {
339475 sha = fileData . sha
340476 }
341477
342- await fetch ( manifestUrl , {
478+ const putResponse = await fetch ( manifestUrl , {
343479 method : 'PUT' ,
344480 headers : {
345481 'Authorization' : `token ${ token } ` ,
346482 'Accept' : 'application/vnd.github.v3+json' ,
347483 'Content-Type' : 'application/json' ,
348484 } ,
349485 body : JSON . stringify ( {
350- message : sha ? `Updated ${ projectId } /manifest.json ` : `Created ${ projectId } /manifest.json ` ,
486+ message : sha ? `Updated ${ projectId } /${ fileName } ` : `Created ${ projectId } /${ fileName } ` ,
351487 content : Buffer . from ( JSON . stringify ( manifest ) ) . toString ( 'base64' ) ,
352488 branch : process . env . BRANCH ,
353489 ...( sha && { sha } ) ,
354490 } )
355491 } )
356492
493+ if ( ! putResponse . ok ) {
494+ const errText = await putResponse . text ( )
495+ throw new Error ( `GitHub upload failed: ${ putResponse . status } - ${ errText } ` )
496+ }
497+
498+ return await putResponse . json ( )
499+
357500 } catch ( error ) {
358- console . error ( `Failed to upload ${ projectId } /manifest.json :` , error )
501+ console . error ( `Failed to upload ${ projectId } /${ fileName } :` , error )
359502 }
360503 }
361504
0 commit comments